iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
自我挑戰組

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

【Day24】模仿知名網站的外觀 X(11) 開發個人資料修改功能

  • 分享至 

  • xImage
  •  

在上回我們完成了個人檔案的顯示,不過還沒有實作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就能看到變化了。


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

尚未有邦友留言

立即登入留言