iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
自我挑戰組

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

【Day22】模仿知名網站的外觀 X(9) 登入實作與推薦追蹤區塊

  • 分享至 

  • xImage
  •  

在未登入時,側邊欄應該不會顯示logout按鈕才對,修正這個錯誤。

修改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 Home from "@/pages/index";
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,
		},
		{
			label: "Profile",
			href: "/users/1",
			icon: FaUser,
		},
	];
	return (
		<div className="col-span-1 h-full pr-4 md:pr-6 flex">
			<div className="flex flex-col items-start">
				<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}
						/>
					))}
					{currentUser && (
						<SidebarItem onClick={() => signOut()} icon={BiLogOut} label="Logout" />
					)}
					<SidebarTweetButton />
				</div>
			</div>
			<Home />
		</div>
	);
};

export default Sidebar;

修改SidebarItem.tsx,處理按鈕事件。

import { useCallback } from "react";
import { IconType } from "react-icons";
import { useRouter } from "next/router";

interface SidebarItemProps {
	label: string;
	href?: string;
	icon: IconType;
	onClick?: () => void;
}

const SidebarItem: React.FC<SidebarItemProps> = ({
	label,
	href,
	icon: Icon,
	onClick,
}) => {
	const router = useRouter();
	const handleClick = useCallback(() => {
		if(onClick){
			return onClick();
		}

		if(href){
			router.push(href);
		}
	}, [router, onClick, href]);

	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" />
			</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>
            </div>
		</div>
	);
};

export default SidebarItem;

啟動專案,畫面上沒有logout了,註冊一個新用戶後,可以看到登出按鈕在畫面上,按下去後可以登出。我們的登入還沒有實作,所以確認logout需要註冊新用戶。

接下來實現登入功能,修改LoginModal.tsx。

'use client'

import useLoginModal from "@/Hooks/useLoginModal";
import { useCallback, useState } from "react";
import Input from "../Input";
import Modal from "../Modal";
import useRegisterModal from "@/Hooks/useRegisterModal";
import { signIn } from "next-auth/react";

const LoginModal = () => {
	const loginModal = useLoginModal();
	const registerModal = useRegisterModal();

	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [isLoading, setIsLoading] = useState(false);

	const onToggle = useCallback(() => {
        if(isLoading){
            return;
        }

        loginModal.onClose();
        registerModal.onOpen();
    }, [isLoading, registerModal, loginModal]);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);

			await signIn("credentials", {
				email,
				password
			});

			loginModal.onClose();
		} catch (error) {
			console.log(error);
		} finally {
			setIsLoading(false);
		}
	}, [loginModal, email, password]);

	const bodyContent = (
		<div className="flex flex-col gap-4">
			<Input
				placeholder="Email"
				onChange={(e) => setEmail(e.target.value)}
				value={email}
				disabled={isLoading}
			/>
			<Input
				placeholder="Password"
				onChange={(e) => setPassword(e.target.value)}
				value={password}
				disabled={isLoading}
			/>
		</div>
	);

	const footerContent = (
        <div className="text-neutral-400 text-center mt-4">
            <p>
                First time using X?
                <span onClick={onToggle} className="text-white cursor-pointer hover:underline">
                    Create an account
                </span>
            </p>
        </div>
    )

	return (
		<Modal
			disabled={isLoading}
			isOpen={loginModal.isOpen}
			title="Login"
			actionLabel="Sign in"
			onClose={loginModal.onClose}
			onSubmit={onSubmit}
			body={bodyContent}
			footer={footerContent}
		/>
	);
};

export default LoginModal;

現在我們可以按下Tweet按鈕登入了。

輸入密碼時,密碼會直接出現在網頁上,這有點風險,進行修改避免密碼曝露。

LoginModal.tsx

'use client'

import useLoginModal from "@/Hooks/useLoginModal";
import { useCallback, useState } from "react";
import Input from "../Input";
import Modal from "../Modal";
import useRegisterModal from "@/Hooks/useRegisterModal";
import { signIn } from "next-auth/react";

const LoginModal = () => {
	const loginModal = useLoginModal();
	const registerModal = useRegisterModal();

	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [isLoading, setIsLoading] = useState(false);

	const onToggle = useCallback(() => {
        if(isLoading){
            return;
        }

        loginModal.onClose();
        registerModal.onOpen();
    }, [isLoading, registerModal, loginModal]);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);

			await signIn("credentials", {
				email,
				password
			});

			loginModal.onClose();
		} catch (error) {
			console.log(error);
		} finally {
			setIsLoading(false);
		}
	}, [loginModal, email, password]);

	const bodyContent = (
		<div className="flex flex-col gap-4">
			<Input
				placeholder="Email"
				onChange={(e) => setEmail(e.target.value)}
				value={email}
				disabled={isLoading}
			/>
			<Input
				placeholder="Password"
				type="password"
				onChange={(e) => setPassword(e.target.value)}
				value={password}
				disabled={isLoading}
			/>
		</div>
	);

	const footerContent = (
        <div className="text-neutral-400 text-center mt-4">
            <p>
                First time using X?
                <span onClick={onToggle} className="text-white cursor-pointer hover:underline">
                    Create an account
                </span>
            </p>
        </div>
    )

	return (
		<Modal
			disabled={isLoading}
			isOpen={loginModal.isOpen}
			title="Login"
			actionLabel="Sign in"
			onClose={loginModal.onClose}
			onSubmit={onSubmit}
			body={bodyContent}
			footer={footerContent}
		/>
	);
};

export default LoginModal;

RegisterModal.tsx

'use client'

import useLoginModal from "@/Hooks/useLoginModal";
import { useCallback, useState } from "react";
import Input from "../Input";
import Modal from "../Modal";
import useRegisterModal from "@/Hooks/useRegisterModal";
import axios from "axios";
import { toast } from "react-hot-toast";
import { signIn } from "next-auth/react";

const RegisterModal = () => {
	const loginModal = useLoginModal();
    const registerModal = useRegisterModal();

	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
    const [name, setName] = useState("");
    const [username, setUsername] = useState("");
	const [isLoading, setIsLoading] = useState(false);

    const onToggle = useCallback(() => {
        if(isLoading){
            return;
        }

        registerModal.onClose();
        loginModal.onOpen();
    }, [isLoading, registerModal, loginModal]);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);
			await axios.post("/api/register", {
				email,
				password,
				username,
				name
			})

			toast.success('Account created.');

			signIn('credentials', {
				email,
				password
			});

			registerModal.onClose();
		} catch (error) {
			console.log(error);
			toast.error('Something went wrong');
		} finally {
			setIsLoading(false);
		}
	}, [registerModal, email, password, username, name]);

	const bodyContent = (
		<div className="flex flex-col gap-4">
			<Input
				placeholder="Email"
				onChange={(e) => setEmail(e.target.value)}
				value={email}
				disabled={isLoading}
			/>
            <Input
				placeholder="Name"
				onChange={(e) => setName(e.target.value)}
				value={name}
				disabled={isLoading}
			/>
            <Input
				placeholder="Username"
				onChange={(e) => setUsername(e.target.value)}
				value={username}
				disabled={isLoading}
			/>
			<Input
				placeholder="Password"
				type="password"
				onChange={(e) => setPassword(e.target.value)}
				value={password}
				disabled={isLoading}
			/>
		</div>
	);

    const footerContent = (
        <div className="text-neutral-400 text-center mt-4">
            <p>
                Already have an account?
                <span onClick={onToggle} className="text-white cursor-pointer hover:underline">
                    Sign in
                </span>
            </p>
        </div>
    )

	return (
		<Modal
			disabled={isLoading}
			isOpen={registerModal.isOpen}
			title="Create an account"
			actionLabel="Register"
			onClose={registerModal.onClose}
			onSubmit={onSubmit}
			body={bodyContent}
            footer={footerContent}
		/>
	);
};

export default RegisterModal;

側邊欄的Notifications和Profile都需要登入才能使用,在未登入時跳出登入視窗提醒登入。

修改SidebarItems.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";

interface SidebarItemProps {
	label: string;
	href?: string;
	icon: IconType;
	onClick?: () => void;
	auth?: boolean;
}

const SidebarItem: React.FC<SidebarItemProps> = ({
	label,
	href,
	icon: Icon,
	onClick,
	auth
}) => {
	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" />
			</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>
            </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 Home from "@/pages/index";
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/1",
			icon: FaUser,
			auth: true
		},
	];
	return (
		<div className="col-span-1 h-full pr-4 md:pr-6 flex">
			<div className="flex flex-col items-start">
				<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>
			<Home />
		</div>
	);
};

export default Sidebar;

接下來,我們要開始處理首頁上的推薦追蹤區塊。

在api資料夾下,新增users資料夾。

在users資料夾下,建立index.ts,從資料庫取得所有的用戶,並從最新排到最舊。

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 users = await prisma.user.findMany({
            orderBy: {
                createdAt: 'desc'
            }
        });

        return res.status(200).json(users);
    } catch(error){
        console.log(error);
        return res.status(400).end();
    }
}

在users資料夾下,建立[userId].ts,取得特定ID用戶的資料。

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 existingUser = await prisma.user.findUnique({
            where: {
                id: userId
            }
        });

        const followersCount = await prisma.user.count({
            where: {
                followingIds: {
                    has: userId
                }
            }
        });

        return res.status(200).json({...existingUser, followersCount })
    } catch(error) {
        console.log(error);
        return res.status(400).end();
    }
}

在Hooks資料夾下,新增useUsers.ts用來快取所有用戶的資料。

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

const useUsers = () => {
const {data, error, isLoading, mutate} = useSWR("/api/users", fetcher)

  return {
    data,
    error,
    isLoading,
    mutate
  }
}

export default useUsers

在Hooks資料夾下,新增useUser.ts用來快取指定ID的用戶的資料。

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

const useUser = (userId: string) => {
const {data, error, isLoading, mutate} = useSWR(userId ? `/api/users/${userId}` : null, fetcher)

  return {
    data,
    error,
    isLoading,
    mutate
  }
}

export default useUser

在Components資料夾下,新增Avatar.tsx,用來顯示用戶頭像。

interface AvatarProps {
    userId: string;
    isLarge?: boolean;
    hasBorder?: boolean;
}

const Avatar: React.FC<AvatarProps> = ({
    userId,
    isLarge,
    hasBorder
}) => {
    return (
        <div></div>
    );
}

export default Avatar;

修改FollowBar.tsx,顯示Avatar和用戶名。

import useUsers from "@/Hooks/useUsers";
import Avatar from "../Avatar";

const FollowBar = () => {
	const { data: users = [] } = useUsers();

	if (users.length === 0) {
		return null;
	}

	return (
		<div className="px-6 py-4 hidden lg:block">
			<div className="bg-neutral-800 rounded-xl p-4">
				<h2 className="text-white text-xl font-semibold">Who to follow</h2>
				<div className="flex flex-col gap-6 mt-4">
					{users.map((user: Record<string, any>) => (
						<div key={user.id} className="flex flex-row gap-4">
							<Avatar userId={user.id} />
							<div className="flex flex-col">
								<p className="text-white font-semibold text-sm">
                  {user.name}
                </p>
                <p className="text-neutral-400 text-sm">
                  @{user.username}
                </p>
							</div>
						</div>
					))}
				</div>
			</div>
		</div>
	);
};

export default FollowBar;

上一篇
【Day21】模仿知名網站的外觀 X(8) 完成註冊功能
下一篇
【Day23】模仿知名網站的外觀 X(10) 推薦追蹤區塊-2與完成個人檔案區塊
系列文
模仿知名網站的外觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言