在未登入時,側邊欄應該不會顯示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;