在pages/api資料夾下,新增comments.tsx,提供一個讓用戶在貼文下發表評論的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") {
return res.status(405).end();
}
try {
const { currentUser } = await serverAuth(req, res);
const { content } = req.body;
const { postId } = req.query;
if(!postId || typeof postId !== 'string'){
throw new Error('Invalid ID');
}
const commment = await prisma.comment.create({
data: {
content,
userId: currentUser.id,
postId
}
});
return res.status(200).json(commment);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
修改Form.tsx,將它打造成可以用來發表貼文和評論的版面,評論後頁面會立刻顯示剛發佈的評論。
import useCurrentUser from "@/Hooks/useCurrentUser";
import useLoginModal from "@/Hooks/useLoginModal";
import usePosts from "@/Hooks/usePosts";
import useRegisterModal from "@/Hooks/useRegisterModal";
import axios from "axios";
import { useCallback, useState } from "react";
import toast from "react-hot-toast";
import Button from "./Button";
import Avatar from "./Avatar";
import usePost from "@/Hooks/usePost";
interface FormProps {
placeholder: string;
isComment?: boolean;
postId?: string;
}
const Form: React.FC<FormProps> = ({ placeholder, isComment, postId }) => {
const registerModal = useRegisterModal();
const loginModal = useLoginModal();
const { data: currentUser } = useCurrentUser();
const { mutate: mutatePosts } = usePosts();
const { mutate: mutatePost } = usePost(postId as string);
const [content, setContent] = useState("");
const [isLoading, setIsLoading] = useState(false);
const onSubmit = useCallback(async () => {
try {
setIsLoading(true);
const url = isComment ? `/api/comments?postId=${postId}` : "/api/posts";
await axios.post(url, { content });
toast.success("Tweet Created");
setContent("");
mutatePosts();
mutatePost();
} catch (error) {
toast.error("Something went wrong");
} finally {
setIsLoading(false);
}
}, [content, mutatePosts, isComment, postId, mutatePost]);
return (
<div className="border-b-[1px] border-neutral-800 px-5 py-2">
{currentUser ? (
<div className="flex flex-row gap-4">
<div>
<Avatar userId={currentUser?.id}/>
</div>
<div className="w-full">
<textarea
disabled={isLoading}
onChange={(e) => setContent(e.target.value)}
value={content}
className="disabled:opacity-80 peer resize-none mt-3 w-full bg-black ring-0 outline-none text-[20px] placeholder-neutral-500 text-white"
placeholder={placeholder}
>
</textarea>
<hr className="opacity-0 peer-focus:opacity-100 h-[1px] w-full border-neutral-800 transition" />
<div className="mt-4 flex flex-row justify-end">
<Button
disabled={isLoading || !content}
onClick={onSubmit}
label="Post"
/>
</div>
</div>
</div>
) : (
<div className="py-8">
<h1 className="text-white text-2xl text-center mb-4 font-bold">
Welcome to X
</h1>
<div className="flex flex-row items-center justify-center gap-4">
<Button label="Login" onClick={loginModal.onOpen} />
<Button label="Register" onClick={registerModal.onOpen} secondary />
</div>
</div>
)}
</div>
);
};
export default Form;
修改[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";
import CommentFeed from "@/Components/posts/CommentFeed";
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"/>
<CommentFeed comments={fetchedPost?.comments} />
</>
)
}
export default PostView
在Components/posts資料夾下,建立CommentFeed.tsx,負責建構評論列表。
import CommentItem from "./CommentItem";
interface CommentFeedProps {
comments?: Record<string, any>[];
}
const CommentFeed: React.FC<CommentFeedProps> = ({ comments = [] }) => {
return (
<>
{comments.map((comment) => (
<CommentItem key={comment.id} data={comment} />
))}
</>
);
};
export default CommentFeed;
在Components/posts資料夾下,建立CommentItem.tsx,用來呈現一則評論,包含了評論者的頭像、名字、用戶名、發布時間和內容。
import { formatDistanceToNowStrict } from "date-fns";
import { useRouter } from "next/router";
import { useCallback, useMemo } from "react";
import Avatar from "../Avatar";
interface CommentItemProps {
data: Record<string, any>;
}
const CommentItem: React.FC<CommentItemProps> = ({ data }) => {
const router = useRouter();
const goToUser = useCallback(
(event: any) => {
event.stopPropagation();
router.push(`/users/${data.user.id}`);
},
[router, data.user.id]
);
const createdAt = useMemo(() => {
if(!data.createdAt){
return null;
}
return formatDistanceToNowStrict(new Date(data.createdAt));
}, [data?.createdAt]);
return (
<div 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 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>
</div>
</div>
);
};
export default CommentItem;
現在啟動專案,進入其中一則貼文,發表評論後,會看到類似以下畫面的內容。
我們的評論區塊已經完成了,接下來我們要開始著手開發通知功能。
修改SidebarItem.tsx,當有通知時會有藍點出現在通知標誌的右上角。
import { useCallback } from "react";
import { IconType } from "react-icons";
import { useRouter } from "next/router";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useLoginModal from "@/Hooks/useLoginModal";
import { BsDot } from "react-icons/bs";
interface SidebarItemProps {
label: string;
href?: string;
icon: IconType;
onClick?: () => void;
auth?: boolean;
alert?: boolean;
}
const SidebarItem: React.FC<SidebarItemProps> = ({
label,
href,
icon: Icon,
onClick,
auth,
alert
}) => {
const loginModal = useLoginModal();
const { data: currentUser} = useCurrentUser();
const router = useRouter();
const handleClick = useCallback(() => {
if(onClick){
return onClick();
}
if(auth && !currentUser){
loginModal.onOpen();
}
else if(href){
router.push(href);
}
}, [router, onClick, href, currentUser, auth, loginModal]);
return (
<div onClick={handleClick} className="flex flex-row items-center">
<div className="relative rounded-full h-14 w-14 flex items-center justify-center p-4 hover:bg-slate-300 cursor-pointer lg:hidden">
<Icon size={28} color="white" />
{alert ? <BsDot className="text-sky-500 absolute -top-4 left-0" size={70} /> : null}
</div>
<div className="relative hidden lg:flex items-center gap-4 p-4 rounded-full hover:bg-slate-300 hover:bg-opacity-10 cursor-pointer">
<Icon size={24} color="white" />
<p className="hidden lg:block text-white text-xl">{label}</p>
{alert ? <BsDot className="text-sky-500 absolute -top-4 left-0" size={70} /> : null}
</div>
</div>
);
};
export default SidebarItem;
修改Sidebar.tsx,通知區塊增加傳入的參數,決定是否顯示藍點。
import { BsHouseFill, BsBellFill } from "react-icons/bs";
import { FaUser } from "react-icons/fa";
import { BiLogOut } from "react-icons/bi";
import SidebarLogo from "./SidebarLogo";
import SidebarItem from "./SidebarItem";
import SidebarTweetButton from "./SidebarTweetButton";
import useCurrentUser from "@/Hooks/useCurrentUser";
import { signOut } from "next-auth/react";
const Sidebar = () => {
const { data: currentUser } = useCurrentUser();
const items = [
{
label: "Home",
href: "/",
icon: BsHouseFill,
},
{
label: "Notifications",
href: "/notifications",
icon: BsBellFill,
auth: true,
alert: currentUser?.hasNotification
},
{
label: "Profile",
href: `/users/${currentUser?.id}`,
icon: FaUser,
auth: true
},
];
return (
<div className="col-span-1 h-full pr-4 md:pr-6">
<div className="flex flex-col items-end">
<div className="space-y-2 lg:w-[230px]">
<SidebarLogo />
{items.map((item) => (
<SidebarItem
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
auth={item.auth}
alert={item.alert}
/>
))}
{currentUser && (
<SidebarItem onClick={() => signOut()} icon={BiLogOut} label="Logout" />
)}
<SidebarTweetButton />
</div>
</div>
</div>
);
};
export default Sidebar;
在api資料夾下,建立notifications資料夾,在notifications資料夾下,新增[userId].ts,根據用戶的 ID,從資料庫中查詢和回傳他們的通知,並將hasNotification設定成false,表示沒有新的通知。
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 { userId } = req.query;
if(!userId || typeof userId !== 'string'){
throw new Error('Invalid ID');
}
const notification = await prisma.notification.findMany({
where: {
userId
},
orderBy: {
createdAt: 'desc'
}
});
await prisma.user.update({
where: {
id: userId
},
data: {
hasNotification: false
}
});
return res.status(200).json(notification);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
在Hooks資料夾下,新增useNotifications.ts,這個程式能夠透過API,取得指定的用戶ID的通知。
import useSWR from "swr";
import fetcher from "@/libs/fetcher";
const useNotifications = (userId?: string) => {
const url = userId ? `/api/notifications/${userId}` : null;
const { data, error, isLoading, mutate } = useSWR(url, fetcher);
return {
data,
error,
isLoading,
mutate
}
}
export default useNotifications
在pages下,新增notifications.tsx,用來呈現使用者收到的新通知。
import { NextPageContext } from 'next';
import { getSession } from "next-auth/react";
import Header from "@/Components/layout/Header";
import NotificationsFeed from "@/Components/NotificationsFeed";
export async function getServerSideProps(context: NextPageContext) {
const session = await getSession(context);
if(!session) {
return {
redirect: {
destination: '/',
permanent: false
}
}
}
return {
props: {
session
}
}
}
const Notifications = () => {
return (
<>
<Header label="Notifications" showBackArrow/>
<NotificationsFeed />
</>
)
}
export default Notifications;
在Components資料夾下,新增NotificationsFeed.tsx,用來設定通知頁面與通知的呈現格式。
import useCurrentUser from "@/Hooks/useCurrentUser";
import useNotifications from "@/Hooks/useNotifications";
import { useEffect } from "react";
import { BsTwitter } from "react-icons/bs";
const NotificationsFeed = () => {
const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser();
const { data: fetchedNotifications = [] } = useNotifications(currentUser?.id);
useEffect(() => {
mutateCurrentUser();
}, [mutateCurrentUser]);
if (fetchedNotifications.length === 0) {
return (
<div className="text-neutral-600 text-center p-6 text-xl">
No notifications
</div>
);
}
return (
<div className="flex flex-col">
{fetchedNotifications.map((notification: Record<string, any>) => (
<div
key={notification.id}
className="flex flex-row items-center p-6 gap-4 border-b-[1px] border-neutral-800"
>
<BsTwitter color="white" size={32} />
<p className="text-white">{notification.content}</p>
</div>
))}
</div>
);
};
export default NotificationsFeed;
修改pages/api/like.ts,有人按下喜歡時就會傳送通知給發文的人。
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);
try{
const post = await prisma.post.findUnique({
where: {
id: postId
}
});
if(post?.userId){
await prisma.notification.create({
data: {
content: "Someone liked your post!",
userId: post.userId
}
});
await prisma.user.update({
where: {
id: post.userId
},
data: {
hasNotification: true
}
})
}
} catch(error){
console.log(error);
}
}
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();
}
}
修改pages/api/commments.ts,有人按下留言就會發送通知。
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") {
return res.status(405).end();
}
try {
const { currentUser } = await serverAuth(req, res);
const { content } = req.body;
const { postId } = req.query;
if(!postId || typeof postId !== 'string'){
throw new Error('Invalid ID');
}
const commment = await prisma.comment.create({
data: {
content,
userId: currentUser.id,
postId
}
});
try{
const post = await prisma.post.findUnique({
where: {
id: postId
}
});
if(post?.userId){
await prisma.notification.create({
data: {
content: "Someone replied your post!",
userId: post.userId
}
});
await prisma.user.update({
where: {
id: post.userId
},
data: {
hasNotification: true
}
})
}
} catch(error){
console.log(error);
}
return res.status(200).json(commment);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
修改pages/api/follow.ts,有人按下追蹤就會發送通知。
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);
try{
await prisma.notification.create({
data: {
content: "Someone followed you",
userId
}
});
await prisma.user.update({
where: {
id: userId
},
data: {
hasNotification: true
}
})
} catch(error){
console.log(error);
}
}
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();
}
}
我們自製的X專案就完成了。