上一回,我們完成了點擊進入貼文區塊的功能,這一次我們要開發追隨和喜歡功能。
在api資料夾,新增follow.ts,提供追隨和取消追隨的API,對資料庫中的用戶追蹤清單進行修改。
import { NextApiRequest, NextApiResponse } from "next";
import serverAuth from "@/libs/serverAuth";
import prisma from "@/libs/prismadb";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST" && req.method !== "DELETE") {
return res.status(405).end();
}
try {
const { userId } = req.body;
const { currentUser } = await serverAuth(req, res);
if (!userId || typeof userId !== "string") {
throw new Error("Invalid ID");
}
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) {
throw new Error("Invalid ID");
}
let updatedFollowingIds = { ...(user.followingIds || []) };
if (req.method === "POST") {
updatedFollowingIds.push(userId);
}
if (req.method === "DELETE") {
updatedFollowingIds = updatedFollowingIds.filter(
(followingId) => followingId !== userId
);
}
const updateUser = await prisma.user.update({
where: {
id: currentUser.id,
},
data: {
followingIds: updatedFollowingIds,
},
});
return res.status(200).json(updateUser);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
在Hooks資料夾下,建立useFollow.ts,透過API實現追蹤和取消追蹤用戶。
import { useCallback, useMemo } from "react";
import useCurrentUser from "./useCurrentUser";
import useLoginModal from "./useLoginModal";
import useUser from "./useUser";
import axios from "axios";
import toast from "react-hot-toast";
const useFollow = (userId: string) => {
const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser();
const { mutate: mutateFetchedUser } = useUser(userId);
const loginModal = useLoginModal();
const isFollowing = useMemo(() => {
const list = currentUser?.followingIds || [];
return list.includes(userId);
}, [userId, currentUser?.followingIds]);
const toggleFollow = useCallback(async () => {
if (!currentUser) {
return loginModal.onOpen();
}
try {
let request;
if(isFollowing){
request = () => axios.delete("/api/follow", {data: {userId}});
}
else{
request = () => axios.post("/api/follow", {userId});
}
await request();
mutateCurrentUser();
mutateFetchedUser();
toast.success("Success");
} catch (error) {
toast.error("Something went wrong");
}
}, [currentUser, isFollowing, userId, mutateCurrentUser, mutateFetchedUser, loginModal]);
return { isFollowing, toggleFollow };
};
export default useFollow;
修改UserBio.tsx,在個人檔案頁面按下寫著Follow的按鈕可以進行追蹤,按下Unfollow可取消追蹤。
import { useMemo } from "react";
import { format } from "date-fns";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useUser from "@/Hooks/useUser";
import Button from "../Button";
import { BiCalendar } from "react-icons/bi";
import useEditModal from "@/Hooks/useEditModal";
import useFollow from "@/Hooks/useFollow";
interface UserBioProps {
userId: string;
}
const UserBio: React.FC<UserBioProps> = ({ userId }) => {
const {data: currentUser} = useCurrentUser();
const {data: fetchedUser} = useUser(userId);
const editModal = useEditModal();
const {isFollowing, toggleFollow} = useFollow(userId);
const createdAt = useMemo(() => {
if(!fetchedUser?.createdAt){
return null;
}
return format(new Date(fetchedUser.createdAt), 'MMMM yyyy');
}, [fetchedUser?.createdAt]);
return (
<div className="border-b-[1px] border-neutral-800 pb-4">
<div className="flex justify-end p-2">
{
currentUser?.id === userId ? (
<Button secondary label="Edit" onClick={editModal.onOpen} />
) : (
<Button secondary={!isFollowing}
label={isFollowing ? "Unfollow" : "Follow"}
onClick={toggleFollow}
outline={isFollowing}
/>
)
}
</div>
<div className="mt-8 px-4">
<div className="flex flex-col">
<p className="text-white text-2xl font-semibold">
{fetchedUser?.name}
</p>
<p className="text-md text-neutral-500">
@{fetchedUser?.username}
</p>
</div>
<div className="flex flex-col mt-4">
<p className="text-white">
{fetchedUser?.bio}
</p>
<div className="flex flex-row items-center gap-2 mt-4 text-neutral-500">
<BiCalendar size={24}/>
<p>
Joined {createdAt}
</p>
</div>
</div>
<div className="flex flex-row items-center mt-4 gap-6">
<div className="flex flex-row items-center gap-1">
<p className="text-white">
{fetchedUser?.followingIds?.length}
</p>
<p className="text-neutral-500">
Following
</p>
</div>
<div className="flex flex-row items-center gap-1">
<p className="text-white">
{fetchedUser?.followersCount || 0}
</p>
<p className="text-neutral-500">
Followers
</p>
</div>
</div>
</div>
</div>
)
}
export default UserBio
接下來要編寫貼文的評論區塊。
在api/posts下,建立[postId].ts,從資料庫裡面取得符合與postId相同的內容後,回傳其中的user和 comments部分。
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/libs/prismadb";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).end();
}
try {
const { postId } = req.query;
if (!postId || typeof postId !== "string") {
throw new Error("Invalid ID");
}
const post = await prisma.post.findUnique({
where: {
id: postId,
},
include: {
user: true,
comments: {
include: {
user: true,
},
orderBy: {
createdAt: "desc",
},
},
},
});
return res.status(200).json(post);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
在Hooks下,新增usePost.ts,透過API取得貼文內容。
import useSWR, { mutate } from "swr";
import fetcher from "@/libs/fetcher";
const usePost = (postId?: string) => {
const url = postId ? `/api/posts/${postId}` : null
const {data, error, isLoading, mutate} = useSWR(url, fetcher)
return {
data,
error,
isLoading,
mutate
}
}
export default usePost
在pages下,建立posts資料夾,在posts資料夾下,新增[postId].tsx,顯示特定貼文的內容和輸入評論的區塊。
import { useRouter } from "next/router";
import { ClipLoader } from "react-spinners";
import usePost from "@/Hooks/usePost";
import Header from "@/Components/layout/Header";
import PostItem from "@/Components/posts/PostItem";
import Form from "@/Components/Form";
const PostView = () => {
const router = useRouter();
const { postId } = router.query;
const { data: fetchedPost, isLoading } = usePost(postId as string);
if(isLoading || !fetchedPost) {
return (
<div className="flex justify-center items-center h-full">
<ClipLoader />
</div>
)
}
return (
<>
<Header label="Post" showBackArrow />
<PostItem data={fetchedPost} />
<Form postId={postId as string} isComment placeholder="Post your reply"/>
</>
)
}
export default PostView
然後我們要實現貼文的喜歡功能。
在pages/api資料夾下,新增like.ts,提供API,當用戶按下喜歡或是取消喜歡時對資料庫中的內容修改。
import { NextApiRequest, NextApiResponse } from "next";
import serverAuth from "@/libs/serverAuth";
import prisma from "@/libs/prismadb";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
){
if(req.method !== 'POST' && req.method !== 'DELETE'){
return res.status(405).end();
}
try{
const { postId } = req.body;
const { currentUser } = await serverAuth(req, res);
if(!postId || typeof postId !== 'string'){
throw new Error('Invalid ID');
}
const post = await prisma.post.findUnique({
where: {
id: postId
}
});
if(!post){
throw new Error('Invalid ID');
}
let updatedLikedIds = [ ...(post.likedIds || []) ];
if(req.method === 'POST'){
updatedLikedIds.push(currentUser.id);
}
if(req.method === 'DELETE'){
updatedLikedIds = updatedLikedIds.filter((likedId) => likedId !== currentUser.id);
}
const updatedPost = await prisma.post.update({
where: {
id: postId
},
data: {
likedIds: updatedLikedIds
}
});
return res.status(200).json(updatedPost);
} catch(error){
console.log(error);
return res.status(400).end();
}
}
在Hooks資料夾下,新增useLIke.ts,使用API實現用戶喜歡和取消喜歡的功能。
import { useCallback, useMemo } from "react";
import useCurrentUser from "./useCurrentUser";
import useLoginModal from "./useLoginModal";
import usePost from "./usePost";
import usePosts from "./usePosts";
import toast from "react-hot-toast";
import axios from "axios";
const useLike = ({ postId, userId }: { postId: string; userId?: string }) => {
const { data: currentUser } = useCurrentUser();
const { data: fetchedPost, mutate: mutateFetchedPost } = usePost(postId);
const { mutate: mutateFetchedPosts } = usePosts(userId);
const loginModal = useLoginModal();
const hasLiked = useMemo(() => {
const list = fetchedPost?.likedIds || [];
return list.includes(currentUser?.id);
}, [currentUser?.id, fetchedPost?.likedIds]);
const toggleLike = useCallback(async () => {
if(!currentUser){
return loginModal.onOpen();
}
try{
let request;
if(hasLiked){
request = () => axios.delete("/api/like", {data: {postId}});
}
else{
request = () => axios.post("/api/like", {postId});
}
await request();
mutateFetchedPost();
mutateFetchedPosts();
toast.success("Success");
} catch(error){
toast.error("Something went wrong");
}
}, [currentUser, hasLiked, postId, mutateFetchedPost, mutateFetchedPosts, loginModal]);
return { hasLiked, toggleLike };
};
export default useLike;
修改PostItem.tsx,完成用戶喜歡和取消喜歡功能。
import { useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import useLoginModal from "@/Hooks/useLoginModal";
import useCurrentUser from "@/Hooks/useCurrentUser";
import { formatDistanceToNowStrict } from "date-fns";
import Avatar from "../Avatar";
import { AiOutlineHeart, AiFillHeart, AiOutlineMessage } from "react-icons/ai";
import useLike from "@/Hooks/useLike";
interface PostItemProps {
data: Record<string, any>;
userId?: string;
}
const PostItem: React.FC<PostItemProps> = ({ data, userId }) => {
const router = useRouter();
const loginModal = useLoginModal();
const { data: currentUser } = useCurrentUser();
const { hasLiked, toggleLike } = useLike({ postId: data.id, userId });
const goToUser = useCallback(
(event: any) => {
event.stopPropagation();
router.push(`/users/${data.user.id}`);
},
[router, data.user.id]
);
const goToPost = useCallback(() => {
router.push(`/posts/${data.id}`);
}, [router, data.id]);
const onLike = useCallback((event: any) => {
event.stopPropagation();
if(!currentUser){
return loginModal.onOpen();
}
toggleLike();
}, [loginModal, currentUser, toggleLike]);
const createdAt = useMemo(() => {
if(!data?.createdAt) {
return null;
}
return formatDistanceToNowStrict(new Date(data.createdAt));
}, [data?.createdAt]);
const LikeIcon = hasLiked ? AiFillHeart : AiOutlineHeart;
return (
<div onClick={goToPost} className="border-b-[1px] border-neutral-800 p-5 cursor-pointer hover:bg-neutral-900 transition">
<div className="flex flex-row items-start gap-3">
<Avatar userId={data.user.id}/>
<div>
<div className="flex flex-row items-center gap-2">
<p onClick={goToUser} className="text-white font-semibold cursor-pointer hover:underline">{data.user.name}</p>
<span onClick={goToUser} className="text-neutral-500 cursor-pointer hover:underline hidden md:block">@{data.user.username}</span>
<span className="text-neutral-500 text-sm">{createdAt}</span>
</div>
<div className="text-white mt-1">
{data.content}
</div>
<div className="flex flex-row items-center mt-3 gap-10">
<div className="flex flex-row items-center text-neutral-500 gap-2 cursor-pointer transition hover:text-sky-500">
<AiOutlineMessage size={20} />
<p>
{data.comments?.length || 0}
</p>
</div>
<div onClick={onLike} className="flex flex-row items-center text-neutral-500 gap-2 cursor-pointer transition hover:text-red-500">
<LikeIcon size={20} color={hasLiked ? "red" : ""}/>
<p>
{data.likedIds?.length}
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default PostItem;
啟動專案,進入貼文按下喜歡,可以看到喜歡數增加了。
我們完成了喜歡的功能,但是評論功能還沒做出來,下一回我們會完成評論功能。