Insta-Next: Likes and Followers
In this part, you will get to build the post liking and user following logics which forms a crucial part of Instagram. In essence, they are just POST requests under the hood.
However, do note that the implementation implemented here is a simplified version and probably doesn't match what they really use.
If you wish to continue from where we dropped off the last part, clone the codes from GitHub.
Overall, we will need 4 APIs, two for each following and liking.
Sneak Peek
Post Liking
API Requests
There are two main APIs that we need to implement here. The first API is for liking and the second will be unliking. (I hope you guessed it)
Since both are about liking a post, they will be under the same route. We will create a handler for both the APIs under post/[post_id]/likes
PS: There's another file in the same folder called likeds.ts
, it's unrelated to what we're doing here
// src/pages/api/posts/[post_id]/likes.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { post_id } = req.query as { post_id: string };
try {
} catch (exception) {
res.status(404).end();
}
}
Look, when it comes to the Prisma level, liking is just creating a UserFollower
object, or at the database level, creating a row in the user_followers
table. Likewise, unliking is just deleting a row in the database.
So you wanna guess what kind of requests will we be receiving? POST and DELETE for liking and unliking respectively! I will not bother you with too many details since they are relatively simple. Here are the actions
// src/features/postLikes/createPostLikes.ts
import prisma from "@/utils/prisma";
const createPostLikes = async (userId: string, postId: string) => {
// This is basically a create if not exist
return await prisma.postLike.upsert({
where: {
// We used a composite primary key,
// so we must nest them under the prismary key
user_id_post_id: {
post_id: postId,
user_id: userId,
},
},
// You can include details in update to make it a
// update if exists, else create
update: {},
create: {
post_id: postId,
user_id: userId,
},
});
};
export default createPostLikes;
// src/features/postLikes/deletePostLikes.ts
import prisma from "@/utils/prisma";
const deletePostLikes = async (userId: string, postId: string) => {
return await prisma.postLike.deleteMany({
where: {
post_id: postId,
user_id: userId,
},
});
};
export default deletePostLikes;
Then, we can get the current user, and use them accordingly in the APIs
// src/pages/api/posts/[post_id]/likes.ts
import createPostLikes from "@/features/postLikes/createPostLikes";
import deletePostLikes from "@/features/postLikes/deletePostLikes";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
// We don't need to return anything back to user, a successful status is enough
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { post_id } = req.query as { post_id: string };
const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id ?? "";
if (req.method == "POST") {
await createPostLikes(userId, post_id);
res.status(201).end();
} else if (req.method == "DELETE") {
await deletePostLikes(userId, post_id);
res.status(204).end();
} else {
res.status(401);
}
}
Frontend
Before we update the component, let's add the corresponding API endpoints into our src/api
// src/api/postLikes.ts
import axios from "axios";
interface PostId {
postId: string;
}
export const likePost = async ({ postId }: PostId): Promise<void> => {
await axios.post(`/api/posts/${postId}/likes`);
};
export const unlikePost = async ({ postId }: PostId): Promise<void> => {
await axios.delete(`/api/posts/${postId}/likes`);
};
Like Component
Let's now go back to the Liked/PostLiked.tsx
component and add the mutation in!
// src/components/posts/Liked/PostLiked.tsx
const PostLiked = ({ likedBy, postId, className = "" }: PostLikedProps) => {
const likePostMutation = useMutation({
mutationFn: likePost,
onSuccess: () => {
showNotification({
message: "You have liked the post!",
color: "green",
});
},
});
const unlikePostMutation = useMutation({
mutationFn: unlikePost,
onSuccess: () => {
showNotification({
message: "You have unliked the post!",
color: "green",
});
},
});
...
However, here comes the big problem, how do I know if the user has liked the post?
It's a challenging question, I believe there are many ways of doing it, and here are some ideas:
When we query the posts, we add another count field that counts for the number of likes coming from the active user (effectively either 0 or 1)
We include an extra filtered relationship in findFollowingPosts to query for
liked_bys
from the active userMake another Prisma query to the database that includes either (1) or (2) and merges two results together.
I hope that it's clear to you that 1 will be the best while 3 will be the worst.
Why?
However, there's no way of implementing (1) right now with Prisma, so we will go with the second alternative, (2).
Prisma still doesn't support renaming fields, so we can't add another field just like that under our _count
// src/features/posts/findFollowingPosts.ts
const findFollowingPosts = async (userId: string) => {
const posts = await prisma.post.findMany({
include: {
user: true,
_count: {
select: {
liked_bys: true,
// if (1) was possible, I can add a liked_by_me
// but no, this select only allows attributes of the table, and no others
},
},
liked_bys: {
where: {
user_id: userId,
},
},
},
orderBy: { created_at: "desc" },
where: {
OR: [
{
user: {
followers: {
some: {
follower_id: userId,
},
},
},
},
{
user_id: userId,
},
],
},
});
...
And don't forget to update the return type of the API,
// src/pages/api/posts/index.ts
...
export type PostWithAuthor = AttachImage<Post, "post"> & {
_count: {
liked_bys: number;
};
liked_bys: PostLike[]; // new attribute
user: AttachImage<User, "user">;
};
...
Then, we can update our PostLiked
to receive an extra prop.
Note that I used another liked
state to keep track of the actual liked as there'll be a delay before the posts are refetched. At the same time, the liked
will determine which mutation function to call and the icon, whether it's filled red or gray outline.
// src/components/posts/Liked/PostLiked.tsx
import { likePost, unlikePost } from "@/api/postLikes";
import openModal from "@/utils/modals/openModal";
import { ActionIcon } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { AiFillHeart, AiOutlineHeart } from "react-icons/ai";
interface PostLikedProps {
likedBy: number;
postId: string;
className?: string;
initialLiked: boolean;
}
const PostLiked = ({
likedBy,
postId,
initialLiked,
className = "",
}: PostLikedProps) => {
// useState here as it'll be changed during mutation
const [liked, setLiked] = useState(initialLiked);
const likePostMutation = useMutation({
mutationFn: likePost,
onSuccess: () => {
showNotification({
message: "You have liked the post!",
color: "green",
});
setLiked(true);
},
});
const unlikePostMutation = useMutation({
mutationFn: unlikePost,
onSuccess: () => {
showNotification({
message: "You have unliked the post!",
color: "green",
});
setLiked(false);
},
});
const mutationFunction = liked ? unlikePostMutation : likePostMutation;
return (
<>
<div>
<ActionIcon
variant="subtle"
onClick={() => {
mutationFunction.mutate({ postId });
}}
loading={mutationFunction.isLoading}
>
{/* Changing the heart, from outline to fill if liked */}
{liked ? (
<AiFillHeart
className="text-red-500 hover:text-red-700"
size="24px"
/>
) : (
<AiOutlineHeart className="hover:text-gray-500" size="24px" />
)}
</ActionIcon>
</div>
<button
className={
"hover:text-gray-700 font-semibold tracking-wider" + className
}
onClick={() =>
openModal({
type: "PostLikes",
innerProps: {
postId,
},
})
}
>
{likedBy} like{likedBy > 1 && "s"}
</button>
</>
);
};
export default PostLiked;
Almost there, we just need to pass the initialLiked
to this PostLiked
component from Post.tsx
component
const Post = ({
post: {
caption,
images,
user,
created_at,
_count: { liked_bys },
id,
liked_bys: likedByMe,
},
}: PostProps) => {
...
<div className="px-2 text-[14px]">
<PostLiked
postId={id}
likedBy={liked_bys}
initialLiked={likedByMe.length > 0}
/>
}
Let's launch our App and like one of the posts! It should be working, the like button should react perfectly
Almost perfect now except for one issue that hopefully you'll notice. The number of like doesn't change immediately when we like the post, why does it happen?
The number of like comes from getAllPost
from here
// src/pages/index.tsx
const posts = useQuery({ queryFn: getAllPosts, queryKey: ["all-posts"] });
which will refetch the queries every now and then (more specifically, depending on refetchInterval
), so the delay will always be there.
Doesn't look that nice to see the number having a delay there, so we can add some codes to update the count when the initialLiked
does not match liked
PS: initialLiked
comes from the server, so it can tell us if the post has the latest data or not when compared with liked
, which is the current state
// src/components/posts/Liked/PostLiked.tsx
const mutationFunction = liked ? unlikePostMutation : likePostMutation;
// Need to adjust since it takes time to refetch the post,
// So before the count is updated, we either add one or subtract one
// based on the liked state, if it isn't updated, which is when
// the initialLiked and liked don't match
const likedByCount =
likedBy + (liked !== initialLiked ? (liked ? 1 : -1) : 0);
return (
...
<button
className={
"hover:text-gray-700 font-semibold tracking-wider" + className
}
onClick={() =>
openModal({
type: "PostLikes",
innerProps: {
postId,
},
})
}
>
{/* Using likedByCount instead of likedBy */}
{likedByCount} like{likedByCount > 1 && "s"}
</button>
...
And it's all good! There's only one last part to update. If you search for the usage of PostLiked
component (or from memory), you should find it in our PostModal
.
Let's update the query used in PostModal
,
// src/features/posts/findSinglePost.ts
// I updated the parameter to include userId
const findSinglePost = async (userId: string, postId: string) => {
const { user, ...post } = await prisma.post.findUniqueOrThrow({
where: {
id: postId,
},
include: {
user: true,
_count: {
select: {
liked_bys: true,
},
},
// included liked_bys filtered relationship
liked_bys: {
where: {
user_id: userId,
},
},
},
});
Likewise, here's the updated API
// src/pages/api/posts/[post_id]/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { AttachImage } from "@/features/images/attach-image";
import { Post, Prisma, User } from "@prisma/client";
import findSinglePost from "@/features/posts/findSinglePost";
import { getServerSession } from "next-auth";
import { authOptions } from "../../auth/[...nextauth]";
export type PostData = {
author: AttachImage<User, "user">;
post: AttachImage<
Prisma.PostGetPayload<{
include: {
_count: {
select: {
liked_bys: true;
};
};
// we don't need to pass where in payload type
// since filtered or not should return the same type
liked_bys: true;
};
}>,
"post"
>;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<PostData>
) {
const { post_id } = req.query as { post_id: string };
const session = await getServerSession(req, res, authOptions);
try {
const data = await findSinglePost(session?.user.id ?? "", post_id);
res.status(200).json(data);
} catch (exception) {
res.status(404).end();
}
}
and done! We can now use the liked_by in our PostModal
// src/components/modals/PostModal.tsx
...
<PostLiked
postId={postId}
likedBy={post.post._count.liked_bys}
initialLiked={post.post.liked_bys.length > 0}
/>
...
You can also head to the profile page to check it out!
Following User
API
Just like the APIs for our post liking, we will need two different APIs. POST and DELETE, each using a separate action.
Since they are basically the same thing, I'll leave it out for you, do check out the GitHub for the solution. I created the actions under src/features/userFollowers
Here's how it should look like on the API handler
// src/pages/api/users/[username]/follows.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "../../auth/[...nextauth]";
import findUserInfo from "@/features/users/findUserInfo";
import createUserFollower from "@/features/userFollowers/createUserFollower";
import deleteUserFollower from "@/features/userFollowers/deleteUserFollower";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { username } = req.query as { username: string };
const user = await findUserInfo(username);
const session = await getServerSession(req, res, authOptions);
const followerId = session?.user.id ?? "";
if (req.method == "POST") {
await createUserFollower(user.id, followerId);
res.status(201).end();
} else if (req.method == "DELETE") {
await deleteUserFollower(user.id, followerId);
res.status(204).end();
} else {
res.status(401);
}
}
Likewise, here are they in the API folders
// src/api/userFollowers.ts
import axios from "axios";
interface UserUsername {
username: string;
}
export const followUser = async ({ username }: UserUsername): Promise<void> => {
await axios.post(`/api/users/${username}/follows`);
};
export const unFollowUser = async ({
username,
}: UserUsername): Promise<void> => {
await axios.delete(`/api/users/${username}/follows`);
};
Frontend
There are mainly two places where the follow button can be seen. The follow button can be found in the LikedUser
component and UserInfo
component.
LikedUser
LikedUser
is found in the modal that pops up when showing the users who liked the post
Just like our PostLikes, we will add a new follow button component.
// src/components/users/follows/FollowUserButton.tsx
import { followUser, unFollowUser } from "@/api/userFollowers";
import { Button } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
interface FollowUserButtonProps {
username: string;
initialFollow: boolean;
}
const FollowUserButton = ({
username,
initialFollow,
}: FollowUserButtonProps) => {
const [following, setFollowing] = useState(initialFollow);
const followUserMutation = useMutation({
mutationFn: followUser,
onSuccess: () => {
showNotification({
message: "You have followed the user!",
color: "green",
});
setFollowing(true);
},
});
const unfollowUserMutation = useMutation({
mutationFn: unFollowUser,
onSuccess: () => {
showNotification({
message: "You have unfollowed the user!",
color: "green",
});
setFollowing(false);
},
});
const mutationFunction = following ? unFollowUser : followUser;
const mutate = () => {
mutationFunction({ username });
};
return following ? (
<Button
className="bg-gray-400 hover:!bg-gray-500"
classNames={{ root: "h-[unset] !px-5", label: "py-2" }}
// what to do here?
onClick={() =>
openModal({
type: unfollowModal,
innerProps: { username, profilePic, onConfirm: mutate },
})
}
loading={mutationFunction.isLoading}
>
Following
</Button>
) : (
<Button
className="bg-blue-500 hover:!bg-blue-600 text-white"
classNames={{ root: "h-[unset] !px-5", label: "py-2" }}
onClick={mutate}
loading={mutationFunction.isLoading}
>
Follow
</Button>
);
};
export default FollowUserButton;
However, I notice that Instagram uses another modal for confirming unfollow,
Let's build the same modal, using our ModalManager
Looking back from earlier parts, we will need to prepare a little when we build modals
// src/utils/modals/types.ts
...
} & {
[key in typeof unfollowModal]: {
onConfirm: () => void;
username: string;
profilePic: string;
};
};
// src/utils/modals/constants.ts
export const postLikesModal = "PostLikes";
export const postModal = "Post";
export const createModal = "Create";
export const storyModal = "Story";
export const unfollowModal = "Unfollow";
export type ModalType =
| typeof postLikesModal
| typeof createModal
| typeof postModal
| typeof storyModal
| typeof unfollowModal;
// src/utils/modals/openModal.ts
const modalProperties: Record<
ModalType,
Omit<Parameters<typeof modals.openContextModal>[0], "modal" | "innerProps">
> = {
...
[unfollowModal]: {},
};
// src/utils/modals/modals.ts
export const modals = {
[postLikesModal]: PostLikedModal,
[postModal]: PostModal,
[storyModal]: StoryModal,
[createModal]: CreateModal,
[unfollowModal]: UnfollowUserModal,
};
And here's the modal that I created, I decided to use the default button since Mantine's buttons are a little troublesome, so yeah
import { unfollowModal } from "@/utils/modals/constants";
import { ModalInnerProps } from "@/utils/modals/types";
import { ContextModalProps, closeModal } from "@mantine/modals";
import ModalLayout from "./ModalLayout";
import { Avatar } from "@mantine/core";
const UnfollowUserModal = ({
innerProps: { onConfirm, username, profilePic },
id,
}: ContextModalProps<ModalInnerProps[typeof unfollowModal]>) => {
return (
<ModalLayout padding={false}>
<div className="px-6 py-8 space-y-4 flex flex-col items-center">
<Avatar
src={profilePic}
alt={username}
size="150px"
classNames={{ root: "rounded-full" }}
className="border-solid border-[1px] border-gray-500"
/>
<p>Unfollow @{username}?</p>
</div>
<div className="flex flex-col items-stretch">
<button
className="py-4 font-semibold text-red-500 hover:bg-red-50 border-t-2 border-solid border-gray-200 active:bg-red-100"
onClick={() => {
onConfirm();
// Remember to close the modal after confirming
closeModal(id);
}}
>
</button>
<button
className="py-4 hover:bg-gray-100 active:bg-gray-200 border-t-2 border-solid border-gray-200"
onClick={() => closeModal(id)}
>
Cancel
</button>
</div>
</ModalLayout>
);
};
export default UnfollowUserModal;
Again, we are facing the issue of how to know if the user is being followed?
Simple, we can just add the include with condition like what we did earlier
// src/features/posts/findPostLikedUsers.ts
import attachImage from "../images/attach-image";
const findPostLikedUsers = async (postId: string, userId: string) => {
const post = await prisma.post.findFirstOrThrow({
where: {
id: postId,
},
include: {
liked_bys: {
include: {
user: {
include: {
followers: {
where: {
follower_id: userId,
},
},
},
},
},
},
},
});
const users = post.liked_bys.map((likedBy) => likedBy.user);
return await Promise.all(
users.map(async (user) => await attachImage(user, "user"))
);
};
export default findPostLikedUsers;
// src/pages/api/posts/[post_id]/likeds.ts
...
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<PostLikedUsersData>
) {
const { post_id } = req.query as { post_id: string };
const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id ?? "";
try {
const users = await findPostLikedUsers(post_id, userId);
res.status(200).json({ users });
} catch (exception) {
res.status(404).end();
}
}
Before proceeding, we should update the types for the user with image and followers, I notice it's being used at many places, so I decided to put it in the shared types.ts
// src/utils/types.ts
export type UserWithFollowersAndImage = AttachImage<
Prisma.UserGetPayload<{
include: {
followers: true;
};
}>,
"user"
>;
Then, you should update the types at a few files
I hope I didn't miss any files...
// src/pages/api/posts/[post_id]/likeds.ts
export type PostLikedUsersData = {
users: UserWithFollowersAndImage[];
};
...
// src/components/users/LikedUser/LikedUser.tsx
interface LikedUserProps {
user: UserWithFollowersAndImage;
}
...
// src/components/users/LikedUser/LikedUsersList.tsx
interface LikedUsersListProps {
users: UserWithFollowersAndImage[];
}
And that's it! We can now use the FollowUserButton component in our LikedUser
component
// src/components/users/LikedUser/LikedUser.tsx
const LikedUser = ({ user }: LikedUserProps) => {
return (
<div className="flex items-center space-x-2">
<Link href={`/users/${user.username}`}>
<Avatar
src={user.profile_pic?.url}
alt={user.username}
radius="xl"
size="44px"
className="hover:brightness-125"
/>
</Link>
<Link href={`/users/${user.username}`}>
<div>{user.username}</div>
<div className="text-gray-500 line-clamp-1 text-sm">
{user.description}
</div>
</Link>
<div className="flex-1" />
<FollowUserButton
username={user.username}
profilePic={user.profile_pic?.url ?? ""}
initialFollow={user.followers.length > 0}
/>
</div>
);
};
Everything seems to work perfectly so far.
However, if you notice, after unfollowing the user, the button doesn't get updated immediately. This means that the button is still showing following when it should be otherwise.
HMM, that's strange didn't we already include the state? Well, it turns out that the modal is destroyed and replaced, it's not like overlapping on top of each other, so this doesn't work here.
Let's see what are some options that we can consider?
Refetch the entire user list, it will certainly work, but a bad option here. Why? I don't think you'll be pleased to see the list being refreshed everytime you follow a user, right? It's annoying.
Stack the modal on top, instead of relying on ModalManager, we can stack the modal somewhere else, which arguably is an okay option.
Store the followed user somewhere, since the modal is destryong and the data is lost, we could store it somewhere, right? What does this mean? State Management. This will be the better approach here.
Since our page is getting a little huge, and option (3) is out of scope (to be revisited in another part), so we will work with option (2) for now.
It's pretty simple, we just stack the Modal in the FollowUserButton
Disclaimer: This solves the issue, but doesn't scale well as each button will have a modal itself. Instead, it would be better to place it in the list or modal level, so that the component is not duplicated. Anyways, this is a temporary solution and we will revisit this issue in the future
Feel free to skip this temporary solution
// src/components/users/follows/FollowUserButton.tsx
...
const FollowUserButton = ({
username,
initialFollow,
profilePic,
}: FollowUserButtonProps) => {
const [following, setFollowing] = useState(initialFollow);
const [showModal, { open, close }] = useDisclosure(false);
const followUserMutation = useMutation({
mutationFn: followUser,
onSuccess: () => {
showNotification({
message: "You have followed the user!",
color: "green",
});
setFollowing(true);
},
});
const unfollowUserMutation = useMutation({
mutationFn: unFollowUser,
onSuccess: () => {
showNotification({
message: "You have unfollowed the user!",
color: "green",
});
setFollowing(false);
},
});
const mutationFunction = following
? unfollowUserMutation
: followUserMutation;
const mutate = () => {
mutationFunction.mutate({ username });
};
return (
<>
{following ? (
<Button
className="bg-gray-400 hover:!bg-gray-500"
classNames={{ root: "h-[unset] !px-5", label: "py-2" }}
onClick={open}
loading={mutationFunction.isLoading}
>
Following
</Button>
) : (
<Button
className="bg-blue-500 hover:!bg-blue-600 text-white"
classNames={{ root: "h-[unset] !px-5", label: "py-2" }}
onClick={mutate}
loading={mutationFunction.isLoading}
>
Follow
</Button>
)}
{/* Added an ugly modal here, to be fixed */}
<Modal
opened={showModal}
padding={0}
closeButtonProps={{ size: 28 }}
radius="lg"
centered={true}
onClose={close}
scrollAreaComponent={Box as any}
classNames={{
header: "absolute bg-transparent top-2 right-2",
close: "!bg-transparent text-black hover:text-gray-800",
inner: "overflow-hidden z-[300]",
content: "!overflow-hidden",
}}
>
<UnfollowUserModal
innerProps={{ username, profilePic, onConfirm: mutate }}
onClose={close}
/>
</Modal>
</>
);
};
export default FollowUserButton;
Unfortunately, we will need to update a few parts to accommodate this change
// src/utils/modals/constants.ts
export type ModalType =
| typeof postLikesModal
| typeof createModal
| typeof postModal
| typeof storyModal;
// | typeof unfollowModal;
// src/utils/modals/modals.ts
export const modals = {
[postLikesModal]: PostLikedModal,
[postModal]: PostModal,
[storyModal]: StoryModal,
[createModal]: CreateModal,
// [unfollowModal]: UnfollowUserModal,
};
// src/utils/modals/openModal.ts
..
},
// [unfollowModal]: {},
};
Finally, let's update the UnfollowUserModal
quickly
// src/components/modals/UnfollowUserModal.tsx
interface UnfollowUserModalProps {
innerProps: ModalInnerProps[typeof unfollowModal];
onClose: () => void;
}
const UnfollowUserModal = ({
innerProps: { onConfirm, username, profilePic },
onClose,
}: UnfollowUserModalProps) => {
return (
<ModalLayout padding={false}>
<div className="px-6 py-8 space-y-4 flex flex-col items-center">
<Avatar
src={profilePic}
alt={username}
size="150px"
classNames={{ root: "rounded-full" }}
className="border-solid border-[1px] border-gray-500"
/>
<p>Unfollow @{username}?</p>
</div>
<div className="flex flex-col items-stretch">
<button
className="py-4 font-semibold text-red-500 hover:bg-red-50 border-t-2 border-solid border-gray-200 active:bg-red-100"
onClick={() => {
onClose();
onConfirm();
}}
>
Unfollow
</button>
<button
className="py-4 hover:bg-gray-100 active:bg-gray-200 border-t-2 border-solid border-gray-200"
onClick={onClose}
>
Cancel
</button>
</div>
</ModalLayout>
);
};
export default UnfollowUserModal;
Now everything works again, life is good, and we can move on!
Disclaimer: Don't use this in real scenario, it's a dirty fix that'll be fixed soon
UserInfo
Likewise, we will need to update the API to pass this info, you can head to findUserInfo.ts
and update accordingly
// src/features/users/findUserInfo.ts
const findUserInfo = async (username: string, userId: string) => {
const user = await prisma.user.findFirstOrThrow({
where: {
username,
},
include: {
_count: {
select: {
followers: true,
followings: true,
posts: true,
},
},
followers: {
where: {
follower_id: userId,
},
},
},
});
return await attachImage(user, "user");
as well as including the active user's ID and updating the type the API is returning
import type { NextApiRequest, NextApiResponse } from "next";
import { AttachImage } from "@/features/images/attach-image";
import { Prisma, User } from "@prisma/client";
import findUserInfo from "@/features/users/findUserInfo";
import { getServerSession } from "next-auth";
import { authOptions } from "../../auth/[...nextauth]";
export type UserInfoData = {
user: AttachImage<
Prisma.UserGetPayload<{
include: {
_count: {
select: {
followers: true;
followings: true;
posts: true;
};
};
followers: true;
};
}>,
"user"
>;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<UserInfoData>
) {
const { username } = req.query as { username: string };
const session = await getServerSession(req, res, authOptions);
try {
const user = await findUserInfo(username, session?.user.id ?? "");
res.status(200).json({ user });
} catch (exception) {
res.status(404).end();
}
}
After getting all the necessary info, you can finally use the FollowUserButton
in the UserInfo
component
// src/components/users/UserInfo.tsx
...
<div className="flex items-end mb-8 space-x-8">
<div className="text-xl">{userInfo.user.username}</div>
<FollowUserButton
initialFollow={userInfo.user.followers.length > 0}
profilePic={userInfo.user.profile_pic?.url ?? ""}
username={userInfo.user.username}
/>
</div>
...
And it actually looks good!
However, the same issue about follower count is happening again, I'll leave it to you as a challenge, but do check out how I solved it on GitHub.
PS: I updated the FollowUserButton
to add an onChange
optional parameter, and other implementations will be equivalent to the previous one in PostLiked
interface FollowUserButtonProps {
username: string;
initialFollow: boolean;
profilePic: string;
onChange?: (following: boolean) => void; // new parameter
}
const FollowUserButton = ({
username,
initialFollow,
profilePic,
onChange,
}: FollowUserButtonProps) => {
...
const followUserMutation = useMutation({
mutationFn: followUser,
onSuccess: () => {
showNotification({
message: "You have followed the user!",
color: "green",
});
setFollowing(true);
// calling them after onSuccess
onChange?.(true);
},
});
const unfollowUserMutation = useMutation({
mutationFn: unFollowUser,
onSuccess: () => {
showNotification({
message: "You have unfollowed the user!",
color: "green",
});
setFollowing(false);
onChange?.(false);
},
});
...
}
PS2: We will also need to fix another API that uses finsUserInfo
// src/pages/api/users/[username]/follows.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { username } = req.query as { username: string };
const session = await getServerSession(req, res, authOptions);
const followerId = session?.user.id ?? "";
const user = await findUserInfo(username, followerId);
Final Touch-up
I don't know if you notice it already, the follow button will continue to show up even if you're the user!
I don't really want to show the button if I'm the user, that's pretty much a bad idea!
Well, there's an easy way to solve it, we will check the active user in the FollowUserButton
and hide it whenever it matches the active user's username
// src/components/users/follows/FollowUserButton.tsx
...
const { data } = useSession();
if (data?.user.name === username) {
return <></>;
}
return (
...
And it'll be all good now!
Summary
In this part, we finally implemented two of the core functions of Instagram: Post Liking and User Following mechanisms. During the process, we created four APIs, a POST and a DELETE request for each.
Finally, we built a separate UI component for both of the mechanisms. You have also learned about keeping the state updated while the request is still being processed.
I also showed you a simple quickfix to get around an issue we encountered where the state can't be updated because the modal is destroyed while switching. We will revisit this issue with state management in a future part!
In the next part, we will look into deploying a Next.js website.
As usual, here's the complete codes for this part: GitHub
Subscribe to my newsletter
Read articles from Hoh Shen Yien directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hoh Shen Yien
Hoh Shen Yien
My name is Hoh Shen Yien, I'm a Malaysian fullstack developer who likes to read and write sometimes 🤩