在上回我們完成了個人檔案的顯示,不過還沒有實作Edit按鈕的事件,這次我要來製作可以修改個人資料和上傳圖片的視窗,用戶可以自由地更改自己的名字、用戶名、自我介紹,以及上傳自己喜歡的頭像或背景圖片。
在api資料夾,新增edit.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 !== 'PATCH'){
return res.status(405).end();
}
try{
const { currentUser } = await serverAuth(req, res);
const { name, username, bio, bannerImage, avatarImage} = req.body;
if(!name || !username){
throw new Error('Missing fields');
}
const updatedUser = await prisma.user.update({
where: {
id: currentUser.id
},
data: {
name,
username,
bio,
avatarImage,
bannerImage
}
});
return res.status(200).json(updatedUser);
} catch(error) {
console.log(error);
return res.status(400).end();
}
}
在Hooks資料下,新增useEditModal.tsx,用來控制EditModal的顯示與關閉。
import { create } from "zustand";
interface EditModalStore{
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const useEditModal = create<EditModalStore>((set) => ({
isOpen: false,
onOpen: () => set({isOpen: true}),
onClose: () => set({isOpen: false}),
}));
export default useEditModal;
修改UserBio.tsx,可以在網頁上按下Edit打開EditModal。
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";
interface UserBioProps {
userId: string;
}
const UserBio: React.FC<UserBioProps> = ({ userId }) => {
const {data: currentUser} = useCurrentUser();
const {data: fetchedUser} = useUser(userId);
const editModal = useEditModal();
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
label="Follow"
onClick={() => {}}
/>
)
}
</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
在Modals資料夾,新增EditModal.tsx。
import { use, useCallback, useEffect, useState } from "react";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useEditModal from "@/Hooks/useEditModal";
import useUser from "@/Hooks/useUser";
import axios from "axios";
import { toast } from "react-hot-toast";
import Modal from "../Modal";
const EditModal = () => {
const {data: currentUser} = useCurrentUser();
const {mutate: mutateFetchedUser } = useUser(currentUser?.id);
const editModal = useEditModal();
const [avatarImage, setAvatarImage] = useState('');
const [bannerImage, setBannerImage] = useState('');
const [name, setName] = useState('');
const [username, setUsername] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
setAvatarImage(currentUser?.avatarImage);
setBannerImage(currentUser?.bannerImage);
setName(currentUser?.name);
setUsername(currentUser?.username);
setBio(currentUser?.bio);
}, [currentUser]);
const [isLoading, setIsLoading] = useState(false);
const onSubmit = useCallback(async () => {
try{
setIsLoading(true);
await axios.patch("/api/edit", {
name,
username,
bio,
avatarImage,
bannerImage
});
mutateFetchedUser();
toast.success('Updated');
editModal.onClose();
} catch(error) {
toast.error("Something went wrong");
} finally {
setIsLoading(false);
}
}, [bio, name, username, avatarImage, bannerImage, editModal, mutateFetchedUser])
return (
<div>
<Modal
disabled={isLoading}
isOpen={editModal.isOpen}
title="Edit your profile"
actionLabel="Save"
onClose={editModal.onClose}
onSubmit={onSubmit}/>
</div>
)
}
export default EditModal
修改_app.tsx,顯示EditModal。
import Layout from "@/Components/Layout";
import LoginModal from "@/Components/Modals/LoginModal";
import RegisterModal from "@/Components/Modals/RegisterModal";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { Toaster } from "react-hot-toast";
import { SessionProvider } from "next-auth/react";
import EditModal from "@/Components/Modals/EditModal";
export default function App({ Component, pageProps }: AppProps) {
return (
<SessionProvider session={pageProps.session}>
<Toaster />
<EditModal />
<RegisterModal />
<LoginModal />
<Layout>
<Component {...pageProps} />
</Layout>
</SessionProvider>
);
}
現在進入個人檔案頁面按下Edit按鈕,就能看到寫著Edit your profile的視窗彈出來。
修改EditModal.tsx,增加在網頁上修改名字、用戶名、自我介紹的功能。
import { use, useCallback, useEffect, useState } from "react";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useEditModal from "@/Hooks/useEditModal";
import useUser from "@/Hooks/useUser";
import axios from "axios";
import { toast } from "react-hot-toast";
import Modal from "../Modal";
import Input from "../Input";
const EditModal = () => {
const {data: currentUser} = useCurrentUser();
const {mutate: mutateFetchedUser } = useUser(currentUser?.id);
const editModal = useEditModal();
const [avatarImage, setAvatarImage] = useState('');
const [bannerImage, setBannerImage] = useState('');
const [name, setName] = useState('');
const [username, setUsername] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
setAvatarImage(currentUser?.avatarImage);
setBannerImage(currentUser?.bannerImage);
setName(currentUser?.name);
setUsername(currentUser?.username);
setBio(currentUser?.bio);
}, [currentUser]);
const [isLoading, setIsLoading] = useState(false);
const onSubmit = useCallback(async () => {
try{
setIsLoading(true);
await axios.patch("/api/edit", {
name,
username,
bio,
avatarImage,
bannerImage
});
mutateFetchedUser();
toast.success('Updated');
editModal.onClose();
} catch(error) {
toast.error("Something went wrong");
} finally {
setIsLoading(false);
}
}, [bio, name, username, avatarImage, bannerImage, editModal, mutateFetchedUser])
const bodyContent = (
<div className="flex flex-col gap-4">
<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="Bio"
onChange={(e) => setBio(e.target.value)}
value={bio}
disabled={isLoading}
/>
</div>
)
return (
<div>
<Modal
disabled={isLoading}
isOpen={editModal.isOpen}
title="Edit your profile"
actionLabel="Save"
onClose={editModal.onClose}
onSubmit={onSubmit}
body={bodyContent}/>
</div>
)
}
export default EditModal
進入專案,點擊Edit按鈕,可以看到三個輸入框,可以進行修改,修改後就能在網頁上看到變化。
接下來,我們開始編寫上傳圖片的部分。
先安裝react-dropzone,來幫助完成拖曳上傳。
npm i react-dropzone
在Components資料夾,新增ImageUpload.tsx,功能是讓用戶可以通過拖曳或點擊上傳圖片。
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
import Image from "next/image";
interface ImageUploadProps {
onChange: (base64: string) => void;
label: string;
value?: string;
disabled?: boolean;
}
const ImageUpload: React.FC<ImageUploadProps> = ({
onChange,
label,
value,
disabled,
}) => {
const [base64, setBase64] = useState(value);
const handleChange = useCallback(
(base64: string) => {
onChange(base64);
},
[onChange]
);
const handleDrop = useCallback(
(files: any) => {
const file = files[0];
const reader = new FileReader();
reader.onload = (event: any) => {
setBase64(event.target.result);
handleChange(event.target.result);
};
reader.readAsDataURL(file);
},
[handleChange]
);
const { getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
onDrop: handleDrop,
disabled,
accept: {
"image/jpeg": [],
"image/png": [],
},
});
return (
<div
{...getRootProps({
className:
"w-full p-4 text-white text-center border-2 border-dotted rounded-md border-neutral-700",
})}
>
<input {...getInputProps()} />
{base64 ? (
<div className="flex items-center justify-center">
<Image src={base64} height="100" width="100" alt="Uploaded image" />
</div>
) : (
<p className="text-white">{label}</p>
)}
</div>
);
};
export default ImageUpload;
修改EditModal.tsx,顯示上傳圖片的區塊。
import { use, useCallback, useEffect, useState } from "react";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useEditModal from "@/Hooks/useEditModal";
import useUser from "@/Hooks/useUser";
import axios from "axios";
import { toast } from "react-hot-toast";
import Modal from "../Modal";
import Input from "../Input";
import ImageUpload from "../ImageUpload";
const EditModal = () => {
const {data: currentUser} = useCurrentUser();
const {mutate: mutateFetchedUser } = useUser(currentUser?.id);
const editModal = useEditModal();
const [avatarImage, setAvatarImage] = useState('');
const [bannerImage, setBannerImage] = useState('');
const [name, setName] = useState('');
const [username, setUsername] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
setAvatarImage(currentUser?.avatarImage);
setBannerImage(currentUser?.bannerImage);
setName(currentUser?.name);
setUsername(currentUser?.username);
setBio(currentUser?.bio);
}, [currentUser]);
const [isLoading, setIsLoading] = useState(false);
const onSubmit = useCallback(async () => {
try{
setIsLoading(true);
await axios.patch("/api/edit", {
name,
username,
bio,
avatarImage,
bannerImage
});
mutateFetchedUser();
toast.success('Updated');
editModal.onClose();
} catch(error) {
toast.error("Something went wrong");
} finally {
setIsLoading(false);
}
}, [bio, name, username, avatarImage, bannerImage, editModal, mutateFetchedUser])
const bodyContent = (
<div className="flex flex-col gap-4">
<ImageUpload
value={avatarImage}
disabled={isLoading}
onChange={(image) => setAvatarImage(image)}
label="Upload avatar image"
/>
<ImageUpload
value={bannerImage}
disabled={isLoading}
onChange={(image) => setBannerImage(image)}
label="Upload banner image"
/>
<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="Bio"
onChange={(e) => setBio(e.target.value)}
value={bio}
disabled={isLoading}
/>
</div>
)
return (
<div>
<Modal
disabled={isLoading}
isOpen={editModal.isOpen}
title="Edit your profile"
actionLabel="Save"
onClose={editModal.onClose}
onSubmit={onSubmit}
body={bodyContent}/>
</div>
)
}
export default EditModal
啟動專案,按下Edit按鈕,可以選擇拖曳圖片到上傳圖片的區塊,或是直接點擊上傳圖片的區塊,上傳成功後可以看到略縮圖,確認後按下Save就能看到變化了。