iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
自我挑戰組

模仿知名網站的外觀系列 第 25

【Day25】模仿知名網站的外觀 X(12) 完成首頁貼文區塊

  • 分享至 

  • xImage
  •  

這次我們要在首頁顯示發佈貼文的輸入框,在輸入區塊的下面會顯示資料庫中儲存的全部貼文,點擊用戶頭像和用戶名會跳到那個用戶的個人檔案頁面。

修改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
		},
		{
			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}
						/>
					))}
					{currentUser && (
						<SidebarItem onClick={() => signOut()} icon={BiLogOut} label="Logout" />
					)}
					<SidebarTweetButton />
				</div>
			</div>
		</div>
	);
};

export default Sidebar;

接著我們要開始開發發表文章的功能,首先在api資料夾下,建立posts資料夾,在posts資料夾下,新增index.ts,這是一個用來處理文章相關的API,POST用來創建新的文章,GET用來取得資料庫中的文章。

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 !== 'GET'){
        return res.status(405).end();
    }

    try{
        if(req.method === 'POST'){
            const { currentUser } = await serverAuth(req, res);
            const { content } = req.body;

            const post = await prisma.post.create({
                data: {
                    content,
                    userId: currentUser.id
                }
            });

            return res.status(200).json(post);
        }

        if(req.method === 'GET'){
            const { userId } = req.query;

            let posts;

            if(userId && typeof userId === 'string'){
                posts = await prisma.post.findMany({
                    where: {
                        userId
                    },
                    include: {
                        user: true,
                        comments: true
                    },
                    orderBy: {
                        createdAt: 'desc'
                    }
                });
            }
            else{
                posts = await prisma.post.findMany({
                    include: {
                        user: true,
                        comments: true
                    },
                    orderBy: {
                        createdAt: 'desc'
                    }
                });
            }
            return res.status(200).json(posts);
        }
    } catch(error) {
        console.log(error);
        return res.status(400).end();
    }
}

在Hooks資料夾下,新增usePosts.ts,用來透過API取得文章。

import useSWR, { mutate } from "swr";
import fetcher from "@/libs/fetcher";

const usePosts = (userId?: string) => {
    const url = userId ? `/api/posts?userId=${userId}` : "/api/posts"
const {data, error, isLoading, mutate} = useSWR(url, fetcher)

  return {
    data,
    error,
    isLoading,
    mutate
  }
}

export default usePosts

在Components資料夾,新增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";

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 [content, setContent] = useState("");
	const [isLoading, setIsLoading] = useState(false);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);
			await axios.post("/api/posts", { content });
			toast.success("Tweet Created");
			setContent("");
			mutatePosts();
		} catch (error) {
			toast.error("Something went wrong");
		} finally {
			setIsLoading(false);
		}
	}, [content, mutatePosts]);
	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 Twitter
					</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;

修改index.tsx,顯示Form。

import Form from "@/Components/Form";
import Header from "../Components/layout/Header";

export default function Home() {
  return (
    <>
      <Header label="Home" />
      <Form placeholder="What's happening?" />
    </>
  )
}

登入後,在首頁輸入貼文內容按下Post就能發表推文。

在Components資料夾下,建立posts資料夾,在posts資料夾下,新增PostFeed.tsx,用來顯示X專案上全部的貼文。

import usePosts from "@/Hooks/usePosts";
import PostItem from "./PostItem";

interface PostFeedProps {
    userId?: string;
}

const PostFeed: React.FC<PostFeedProps> = ({userId}) => {
    const {data: posts = []} = usePosts(userId);

  return (
    <>
        {
            posts.map((post: Record<string, any>) => (
                <PostItem 
                    userId={userId}
                    key={post.id}
                    data={post}
                />
            ))
        }
    </>
  )
}

export default PostFeed

在posts資料夾下,新增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, AiOutlineMessage } from "react-icons/ai";

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 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();

        loginModal.onOpen();
    }, [loginModal]);

    const createdAt = useMemo(() => {
        if(!data?.createdAt) {
            return null;
        }
        return formatDistanceToNowStrict(new Date(data.createdAt));
    }, [data?.createdAt]);

	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-sky-500">
                            <AiOutlineHeart size={20} />
                            <p>
                                {data.likedIds?.length || 0}
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default PostItem;

修改index.tsx,顯示PostFeed區塊。

import Form from "@/Components/Form";
import Header from "../Components/layout/Header";
import PostFeed from "@/Components/posts/PostFeed";

export default function Home() {
  return (
    <>
      <Header label="Home" />
      <Form placeholder="What's happening?" />
      <PostFeed />
    </>
  )
}

接下來,我們要在個人頁面上顯示該用戶所發表的貼文,修改[userId].tsx。

import { useRouter } from "next/router";
import { ClipLoader } from "react-spinners";
import Header from "@/Components/layout/Header";
import useUser from "@/Hooks/useUser";
import UserHero from "@/Components/users/UserHero";
import UserBio from "@/Components/users/UserBio";
import PostFeed from "@/Components/posts/PostFeed";

const UserView = () => {
    const router = useRouter();
    const { userId } = router.query;

    const { data: fetchedUser, isLoading} = useUser(userId as string);

    if(isLoading || !fetchedUser){
        return (
            <div className="flex justify-center items-center h-full">
                <ClipLoader color="lightblue" size={80}/>
            </div>
        )
    }

    return (
        <>
            <Header showBackArrow label={fetchedUser?.name} />
            <UserHero userId={userId as string}/>
            <UserBio userId={userId as string}/>
            <PostFeed userId={userId as string}/>
        </>
    )
}

export default UserView;

啟動專案,在首頁上點擊其中一則貼文中的用戶頭像或用戶名可以進去用戶的個人檔案。

在個人檔案頁面上可以看到用戶發的所有貼文。


上一篇
【Day24】模仿知名網站的外觀 X(11) 開發個人資料修改功能
下一篇
【Day26】模仿知名網站的外觀 X(13) 完成追隨和喜歡功能
系列文
模仿知名網站的外觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言