Hey dev, long time no see! I hope you had a great holiday. Today, we’re going to continue working on our Instagram clone project.
Our plan is to create a user profile and display user's gallery, and we’ll load the data from the Node.js backend we built in the previous article.
The source code for the front-end you can find here.
Let’s get started!
Set Up our React application
As usual, to start our front end, I’m going to create a new React app. I want to keep both the front end and back end in the same parent directory, which will be the demo folder. My back end is located in the instaClone folder, and I’ll run the Vite build command to create the React app in demo directory.
$ npm create vite@latest
During the command execution, we need to give our project a name. We are going to call it insta-fe, select React as the framework, choose React Router, select no roll-down, and install the dependencies.
After that, if we run the app using npm run dev, we can see the default React Router page in the browser at http://localhost:5173. You can see the result on the picture below:
Let's go ahead and update the Home component and display a simple “Hey” instead of the default React Router content. To do this, go to routes/home.tsx and pass this code:
// routes/home.tsx
import type { Route } from "../+types/home";
import { Welcome } from "../../welcome/welcome"; // You can remove this one
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Home() {
return "Hey"; // Update the return statement
}
Now, our root page will return us just an empty page with the text "Hey".
Creating Profile component and Routing
According to our application design, we want it to have multiple "pages": one for the feed and another for their own profile. Each page will send the appropriate API request to the backend to retrieve the required data.
So, let’s create a new profile route that accepts a dynamic id parameter and returns the Profile component.
// routes.tsx
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"), // register route for our root/index page (home)
route("/profile/:id", "./routes/profile.tsx") // register route so localhost:port/profile/3546 will return us Profile Component
] satisfies RouteConfig;
Here we have our default (index) route, where the Home page is returned. We also created a new route using the route method to define the route itself, and the second parameter represents the Profile component that will be returned when a user navigates to this route.
Now, we can create a placeholder for our Profile component. Let’s create a new folder named components inside the routes folder and place a new file named Profile.tsx inside it. Also, move Home.tsx there as well.
Inside Profile.tsx place this code snippet:
import { useParams } from "react-router";
export default function Profile() {
const { id } = useParams<{id: string}>();
return(
<>
<h1>User Profile {id} </h1>
</>
)
}
Here, we define the id parameter using the React hook useParams, which allows us to capture the dynamic part of the route that we defined in our routes.tsx file.
Also, since we moved Home.tsx and Profile.tsx into the components folder, we need to update our routes.tsx file as well.
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/components/home.tsx"), // adding components folder
route("/profile/:id", "./routes/components/profile.tsx") // adding components folder here as well
] satisfies RouteConfig;
Now, if we visit our localhost:5173/profile/123, we will see something like that:
Connecting with the backend and display results
Defining UserProfile and Image Types
After we have hooked up our front end, we can now send requests to our API to retrieve user data and display it on the page. We have to update out Profile.tsx and define how the user profile is going to look like, so we can easily read it from the backend and apply.
Let's create a new folder in root directory a nd name it types. Inside we create a new file named UserProfile.ts. Inside we place the following code:
export type UserProfile = {
id: string;
username: string;
userAcountName: string
numberOfPosts: number;
numberOfFollowers: number;
numberOfFollowing: number;
profileDescription: string;
threadName: string | null;
ProfileImgSrc: string;
ListOfPosts: Images[]; // this one is a new type, we don't have it yet
}
As you can see, our structure is very similar to what we defined in the backend. You may also notice that we have ListOfPosts with a type of Images. This is another type that we need to create, and every image in the application will have these properties.
So, inside the types folder, let’s create a new file and name it Images.ts. Inside it, we are going to define the properties for this type:
export type Images = {
id: string;
imgSrc: string;
author: string;
type: string;
};
As you can see, it’s pretty straightforward. Every image in a post will have its own id, source, author, and type. Later on, we are going to add a Post definition that will use the Image type itself. For now, however, we are using it only in the Profile component.
Sending API call to our backend
Now that we have a defined shape for our userProfile data, we can update our Profile component and call the API. So, let’s add the following code to our Profile component:
import { useParams } from "react-router";
import { useEffect, useState } from "react";
import type { UserProfile } from "~/types/UserProfile";
export default function Profile() {
const { id } = useParams<{ id: string }>();
// Add useState hook to control userData
const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
// Execute the profile function every time when id is changing (like new route )
useEffect(() => {
getUserProfile(id);
}, [id]);
// Call our backend and pass current userId
const getUserProfile = async (userId: string | undefined) => {
try {
const response = await fetch(`http://localhost:3000/user/${userId}`);
if (!response.ok) throw new Error("Failed to fetch user");
const data: UserProfile = await response.json();
setCurrentUser(data);
} catch (error) {
console.error(error);
}
};
// Display username we just received from our backend
return (
<>
<p>{currentUser.username}</p>
</>
);
}
Let’s review what is happening in the code. First, we define a new React state variable called currentUser. When we click on a user account, the route changes and the id parameter is updated. This id is then sent to our backend to retrieve the user data.
We use the useEffect hook to call the API every time the id in the route changes. After the data is received, our front end displays the newly fetched currentUser username.
Remember, currentUser is of type UserProfile, which we just created, and it matches the backend structure. This allows us to safely access properties like currentUser.username and display the correct data. If we run both back-end and front-end together, and visit userProfile/123 we should see this:
As you may remember, this username comes from our mocked backend data that we send to the front end.
Now we can go ahead and actually build our Instagram UserProfile UI.
Matching Instagram User profile UI and our front-end
If we look at the origin application, the user profile has a header where user details are displayed, such as the user avatar, follower count, number of posts, and more. We are going to create a separate component for this section, and call it Header.
I am going to create a new folder for UI elements and name it ui. Inside it, I will create a new file called Header.tsx. Inside this file, we will add the following code:
import type { UserProfile } from "~/types/UserProfile";
import AddIcon from "@mui/icons-material/Add";
import LockIcon from "@mui/icons-material/Lock";
import MenuIcon from "@mui/icons-material/Menu";
import AddCircleIcon from "@mui/icons-material/AddCircle";
interface HeaderProps {
user: UserProfile | null;
}
export const Header = ({ user }: HeaderProps) => {
return (
<header>
<section className="adding_new_post_container">
<button>
<AddIcon fontSize="large"></AddIcon>
</button>
<p>
<button>
<LockIcon />
</button>
<b>{user?.username}</b>
</p>
<button>
<MenuIcon />
</button>
</section>
<section className="user_data_container">
<section className="user_image_container">
<img src={user?.ProfileImgSrc} alt="user-profile" />
<AddCircleIcon />
</section>
<section className="user_data_info_wrapper">
<p>
<b>{user?.userAccountName}</b>
</p>
<div className="user_details">
<div>
<p>
<b>{user?.numberOfPosts}</b>
</p>
<p>posts</p>
</div>
<div>
<p>
<b>{user?.numberOfFollowers}</b>
</p>
<p>followers</p>
</div>
<div>
<p>
<b>{user?.numberOfFollowing}</b>
</p>
<p>following</p>
</div>
</div>
</section>
</section>
</header>
);
};
We created several elements in our header, such as the number of posts, the profile image, and the follower count. Now, let’s make sure they are displayed on the user profile page and update the UserProfile component.
/// UserProfile.tsx file
/// the rest of the code
return (
<>
<Header user={currentUser}></Header>
</>
);
If we go to our UserProfile route we should see something like this:
Let's go ahead and complete the rest of the Header. Bu adding description for the account, and buttons for sharing and editing.
// Header.tsx continues
<div>
<p>
<b>{user?.numberOfFollowing}</b>
</p>
<p>following</p>
</div>
</div>
</section>
</section>
<div className="description-container">
<p>{user?.profileDescription}</p>
</div>
<div className="thread-container">
<p>
<b>{user?.threadName ? `@${user?.threadName}` : ''} </b>
</p>
</div>
<section className="user_profile_buttons">
<div>
<button>Edit Profile</button>
</div>
<div>
<button>Share Profile</button>
</div>
<div>
<button id="person_icon">
<PersonAddIcon />
</button>
</div>
</section>
</header>
Our UserProfile header is ready. Now we can display all the posts that the user has in the gallery. For this, we will create a new component in the ui folder and name it UserGallery.tsx.
Place this code inside:
import { useState } from "react";
import "../../styles/components/UserGallery.css";
import GridOnIcon from "@mui/icons-material/GridOn";
import SmartDisplayIcon from "@mui/icons-material/SmartDisplay";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import type { Images } from "~/types/Images";
interface GalleryProps{
posts: Images[] | undefined
}
export const UserGallery = ({posts} : GalleryProps) => {
const [UserPosts, setUserPosts] = useState<Images[]>([]);
const [selectedMediaType, setSelectedMediaType] = useState("post");
return (
<>
<section className="gallery_switch_button_container">
<button >
<GridOnIcon fontSize="large" sx={{ color: "black"}} />
</button>
<button>
<SmartDisplayIcon fontSize="large" sx={{ stroke: "#BDBDBD", fill: "transparent", strokeWidth: 1.7 }}/>
</button>
<button>
<AssignmentIndIcon fontSize="large" sx={{ stroke: "#BDBDBD", fill: "transparent", strokeWidth: 1.7 }}/>
</button>
</section>
<section className="gallery_post">
{/*Posts with images*/}
{posts?.map((post) => (
<div>
<img src={post.imgSrc} alt={post.author + " image"} />
</div>
))}
</section>
</>
);
};
Here, we define the gallery props, and the only property is posts. We also use two useState hook variables: one for UserPosts and another for the selected media type.
In the return statement, we have a section with three buttons that display different types of content: posts, reels, and posts in which the user was mentioned.
The final part maps our posts to the actual images.
Now, we can add this component to our UserProfile page, so updated code will look like this:
// other imports stay the same
import { UserGallery } from "./components/UserGallery";
export default function Profile() {
const { id } = useParams<{ id: string }>();
const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
useEffect(() => {
getUserProfile(id);
}, [id]);
// Load user Data
const getUserProfile = async (userId: string | undefined) => {
try {
const response = await fetch(`http://localhost:3000/user/${userId}`);
if (!response.ok) throw new Error("Failed to fetch user");
const data: UserProfile = await response.json();
setCurrentUser(data);
} catch (error) {
console.error(error);
}
};
return (
<>
<Header user={currentUser} />
<UserGallery posts={currentUser?.ListOfPosts}></UserGallery> {/*new line*/}
</>
);
}
Here we adding UserGallery and passing retrieved posts from our backend.
Adding Styles
Since our pages currently have no styles, we need to fix that. Let’s create a new folder in the root directory and name it styles. Inside it, add two new files: HeaderStyle.css and UserGalleryStyle.css.
Also, import HeaderStyle.css into Header.tsx and UserGalleryStyle.css to UserGallery.tsx
// Header.tsx
import "../styles/HeaderStyle.css"; // add this line
//rest of the code is the same
We’ll start styling our Header first. Inside the HeaderStyle.css file, add the following code:
header{
padding: 5px 20px;
}
.adding_new_post_container{
display: flex;
justify-content: space-between;
}
.adding_new_post_container p{
display: flex;
align-items: center;
}
.user_data_container{
display:flex;
margin-top:1em;
gap:1em;
}
.user_image_container{
position: relative;
}
.user_image_container img{
width:110px;
object-fit: cover;
height: 90px;
border-radius: 100px;
padding: 5px;
border: 4px solid rgba(0, 0, 0, 0.44);
}
.user_image_container button{
position: absolute;
bottom:2px;
padding:0;
right: 2px;
}
.user_data_info_wrapper{
width:90%;
}
.user_data_info_wrapper .user_details{
display:flex;
width:70%;
justify-content: space-between;
gap:10px;
font-size: 0.9em;
line-height: 1.2;
}
.user_name{
line-height: 2;
font-size: 0.9em;
}
.user_profile_buttons{
display:flex;
gap:10px;
margin:1em 0;
}
.user_profile_buttons div button{
background-color: rgb(235, 232, 229);
padding: 2px 50px;
font-weight: 600;
border-radius: 5px;
}
#person_icon{
padding: 1px 10px;
}
.description-container{
font-size: 0.9em;
}
.description-container p{
margin: 1em 0 0.2em;
font-size: 0.9em;
}
.thread-container{
font-size: 0.9em;
}
And the code for the UserGallery styles will look like this:
.gallery_post{
display: grid;
grid-template-columns: 33% 33% 33%;
gap:2px;
}
.gallery_post div img{
width:100%;
object-fit: cover;
height: 190px;
}
.gallery_switch_button_container{
display: flex;
margin: 1em 0;
align-items: center;
justify-content: space-around;
}
After all these manipulations, our result should look like this for localhost:5173/profile/1234:
and this for localhost:5173/profile/123:
In this article, we fetched our mocked data from the backend and created a layout for the UserProfile data. In the next articles, we will add a database, set up our backend to read from it directly, and then display the results to the users.
Thank you for sticking with me during this tutorial. Happy Coding 🚀
Comments