一樣先在pages
資料夾底下建立Profile.js
在這個頁面我們需要根據路由的使用者ID來取得當前作者的資料和他發布的文章。
import { useEffect,useState } from 'react';
import { useParams } from 'react-router-dom';
import PostItem from '../components/PostItem';
import api from '../api/api';
const ProfilePage = (props) => {
//作者發布的文章資料
const [postList, setPostList] = useState([]);
const [loading, setLoading] = useState(true);
//作者的個人資料
const [author, setAuthor] = useState({});
//作者的ID
const { userId } = useParams();
//當前登入的使用者ID
const currentUserId = JSON.parse(localStorage.getItem("user")).data.id;
useEffect(() => {
//要確保兩個api都呼叫完成後才將Loading動畫關閉
Promise.all([
getUserInfo(),
getUserPosts()
])
.then(([userInfo, userPosts]) => {
setAuthor(userInfo);
setPostList(userPosts);
})
.catch((error) => {
alert('An error occurred:', error.message);
console.error(error);
})
.finally(() => {
setLoading(false);
});
},[])
//取得作者資訊
const getUserInfo = () =>{
return api.get(`users/${userId}/profile`)
.then((result) => {
return result.data;
});
} ;
//取得作者發布的文章
const getUserPosts = () =>{
return api.get(`users/${userId}/posts`)
.then((result) => {
return result.data;
});
} ;
//若資料還在載入中
if (loading) {
return <div className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-4xl">
Loading...
</div>;
}
return <div className="w-[600px] mx-auto mt-20">
<div className="flex items-center">
{/*...*/}
<div className="ml-4">
<p className="font-bold text-xl">{author.fullName}</p>
<p>{author.bio}</p>
<p className="text-sm text-gray-400">Join Date: <span className="ml-1">{author.joinDate}</span></p>
{/* 判斷當前作者是不是和登入者相同,是的話顯示編輯個人資料按鈕*/}
{
currentUserId === userId &&
<button className="text-violet-600 cursor-pointer hover:text-violet-800">Edit Profile</button>
}
</div>
</div>
<div className="py-8">
{ postList.map(post => <PostItem key={post._id} post={post} id={post._id}/>)}
</div>
</div>
};
export default ProfilePage;
//App.js
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
...
import ProfilePage from './pages/Profile';
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout/>,
children:[
{...}
{ path: '/profile/:userId', element: <ProfilePage />},
]
},
{
path: '/login',
element: <Login/>,
},
{
path: '/register',
element: <Register/>,
}
])
const App = () => {
...
}
export default App;
當我們點選該作者的名字時應該要能導到作者資訊頁,此外在這裡也新增了
isShowAuthor
,判斷是否要顯示作者名稱,因為在使用者資訊頁時,不需要顯示作者名字(因為那邊會撈回的文章資料就是該作者的)。
<Link to={`/profile/${id}`}>
<p className="text-violet-600 ml-2 text-sm">
{author.fullName}
</p>
</Link>
PostItem程式碼
import Tag from "./Tag";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
const PostItem = ({ post, id, isShowAuthor }) => {
{/*...*/}
return (
<div className="mb-8">
<Link to={`/posts/${id}`}>
{/*...*/}
</Link>
{isShowAuthor ? (
<div className="flex items-center mt-4">
{author.profileImage ? (
<div className="w-[28px] h-[28px] rounded-full border border-gray-200 overflow-hidden">
<img src={author.profileImage} alt="avatar" />
</div>
) : (
<div className="w-[32px] h-[32px] rounded-full border bg-gray-400 text-white text-center leading-[32px] overflow-hidden">
{author.fullName[0]}
</div>
)}
{/* 到使用者資訊頁的連結 */}
<Link to={`/profile/${author._id}`}>
<p className="text-violet-600 ml-2 text-sm">{author.fullName}</p>
</Link>
<p className="text-sm text-gray-400 ml-3 text-sm">{createdDate}</p>
</div>
) : (
<p className="text-sm text-gray-400 py-2 text-sm">{createdDate}</p>
)}
</div>
);
};
export default PostItem;
🔺同上,HomeItem.js
和TagItem.js
裡有作者名稱也要記得加上作者資訊頁的連結。
因為加了isShowAuthor
的屬性,在PostList.js
裡的PostItem
也要加上這個屬性
//PostList.js
const PostList = props =>{
return ...(略)
<div className="py-8">
{ postList.map(post => <PostItem key={post._id} post={post} id={post._id} isShowAuthor={true}/>)}
</div>
}
export default PostList;
在pages
底下新增EditProfile.js
//App.js
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
{...}
import EditProfile from './pages/EditProfile'; //路由設置
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout/>,
children:[
{...}
{ path: '/profile/edit', element: <EditProfile />},
]
},
{...}
])
const App = () => {
...
}
export default App;
更新使用者資訊和更新文章內容其實很類似,頭像圖片的處理就和文章封面圖片一樣。
一樣先設定會用到的變數
const navigate = useNavigate();
const validateRequired = (value) => {
if (value.trim() === "") {
return { isValid: false };
}
return { isValid: true };
};
const [profileImage, setProfileImage] = useState("");
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
//使用者姓名
const {
value: fullName,
isValid: fullNameIsValid,
hasError: fullNameInputHasError,
valueChangeHandler: fullNameChangeHandler,
inputBlurHandler: fullNameBlurHandler,
setEnteredValue: setFullName,
} = useInput(validateRequired);
//使用者簽名檔,因為非必填所以不會存取hasError跟isValid
const {
value: bio,
valueChangeHandler: bioChangeHandler,
inputBlurHandler: bioBlurHandler,
setEnteredValue: setBio,
} = useInput((value) => {
return true;
});
接著進頁面時要透過當前登入使用者的Id來取得資料
//從當前localstorage取得使用者id
const userId = JSON.parse(localStorage.getItem("user")).data.id;
useEffect(() => {
getUserInfo();
}, []);
//呼叫API取得使用者資料
const getUserInfo = () =>{
api.get(`/users/${userId}/profile`)
.then((result) =>{
const author = result.data;
//設定資料
setAuthor(author);
setProfileImage(author.profileImage);
setFullName(author.fullName);
setBio(author.bio);
})
.catch((error) =>{
alert('An error occurred:',error.message);
console.error(error);
});
}
其他部分和更新文章一樣,這裡就直接附上完整的程式碼
import api from "../api/api";
import useInput from "../hooks/useInput";
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { AiFillEdit } from "react-icons/ai";
const EditProfile = (props) => {
const navigate = useNavigate();
//從當前路由取得使用者id
const { userId } = useParams();
const validateRequired = (value) => {
if (value.trim() === "") {
return { isValid: false };
}
return { isValid: true };
};
const [profileImage, setProfileImage] = useState("");
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const {
value: fullName,
isValid: fullNameIsValid,
hasError: fullNameInputHasError,
valueChangeHandler: fullNameChangeHandler,
inputBlurHandler: fullNameBlurHandler,
setEnteredValue: setFullName,
} = useInput(validateRequired);
const {
value: bio,
valueChangeHandler: bioChangeHandler,
inputBlurHandler: bioBlurHandler,
setEnteredValue: setBio,
} = useInput((value) => {
return true;
});
useEffect(() => {
getUserInfo();
}, []);
const getUserInfo = () =>{
api.get(`/users/${userId}/profile`)
.then((result) =>{
const author = result.data;
setAuthor(author);
setProfileImage(author.profileImage);
setFullName(author.fullName);
setBio(author.bio);
})
.catch((error) =>{
alert('An error occurred:',error.message);
console.error(error);
});
}
//更新使用者資料
const updateProfile = async () => {
try {
const profile = {
fullName,
bio,
profileImage,
};
await api.put(`/users/${userId}/profile`, profile);
} catch (error) {
throw error;
}
};
//頭像圖片上傳
const uploadImage = async () => {
const formData = new FormData();
formData.append("image", file);
try {
const response = await api.post("/images/upload", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data.data.url;
} catch (error) {
throw error;
}
};
//選擇檔案時的處理
const onFileChange = (e) => {
const selectedFile = e.target.files[0];
console.log(selectedFile);
setFile(selectedFile);
// 如果使用者選擇了圖片,則生成一個預覽URL並設定它
if (selectedFile) {
const reader = new FileReader();
reader.onload = function (event) {
setProfileImage(event.target.result);
};
reader.readAsDataURL(selectedFile);
}
};
//Submit
const onSubmit = async (e) => {
e.preventDefault();
if (!formIsValid) return;
//若沒有選擇圖片或當前圖片url為null時(如果本來就沒有頭像應該為空字串)
if (profileImage === null && file === null) {
alert("Please select image!");
return;
}
setLoading(true);
try {
//判斷圖片url,如果file有資料就表示使用者有選取新的圖片
const imageUrl = file ? await uploadImage() : profileImage;
await updateProfile(imageUrl);
alert("Profile update successfully!");
navigate(`/profile/${userId}`);
} catch (error) {
console.error("An error occurred:", error);
alert("An error occurred:", error.message);
} finally {
setLoading(false);
}
};
let formIsValid = false;
if (fullNameIsValid) {
formIsValid = true;
}
const fullNameInputClasses = fullNameInputHasError
? "border-red-500 focus:ring-red-500"
: "border-violet-300 focus:ring-violet-500";
const avatarClasses = profileImage
? "border-gray-200"
: "bg-gray-400 text-white text-center text-3xl leading-[100px]";
return (
<div className="w-[400px] mx-auto mt-20 rounded mt-8 p-8 shadow-xl border border-gray-200">
<section className="mb-8 flex justify-center relative">
<div
className={`w-[100px] h-[100px] rounded-full overflow-hidden border border-gray-200 relative ${avatarClasses}`}
>
{profileImage ? (
<img src={profileImage} alt="profile" />
) : (
<span> {author.fullName[0]}</span>
)}
</div>
<label className="absolute rounded-full border border-gray-300 bg-white p-1 text-gray-800 text-sm right-[36%] bottom-2 cursor-pointer">
<AiFillEdit />
<input
className="hidden"
type="file"
accept="image/jpeg, image/png"
onChange={onFileChange}
/>
</label>
</section>
<form onSubmit={onSubmit}>
<div className="mb-4">
<h3 className="text-l mb-1">FullName</h3>
<input
type="text"
value={fullName}
placeholder="Your FullName"
onChange={fullNameChangeHandler}
onBlur={fullNameBlurHandler}
className={`mt-1 block w-full px-3 py-2 bg-white border
rounded-md text-sm shadow-sm placeholder-slate-400
focus:outline-none focus:ring-1 ${fullNameInputClasses}`}
/>
{fullNameInputHasError && (
<p className="text-red-500 text-sm">FullName is required</p>
)}
</div>
<div className="mb-8">
<h3 className="text-l mb-1">Bio</h3>
<textarea
rows={5}
value={bio}
placeholder="Bio"
onChange={bioChangeHandler}
onBlur={bioBlurHandler}
className="mt-1 block w-full px-3 py-2 bg-white border
rounded-md text-sm shadow-sm placeholder-slate-400
focus:border-violet-300 focus:ring-violet-500
focus:outline-none focus:ring-1"
></textarea>
</div>
<button
type="submit"
disabled={loading || !formIsValid}
className="px-4 py-2 bg-violet-600 hover:bg-violet-700 duration-200 text-white w-full rounded cursor-pointer disabled:opacity-50 disabled:cursor-auto"
>
{loading ? "Uploading..." : "Update"}
</button>
</form>
</div>
);
};
export default EditProfile;
編輯使用者頁面可以透過作者資訊頁(Profile.js
)裡的EditProfile button進入,另外也能從Header.js
的Account Setting進入,所以我們要去這兩個檔案設定路由到EditProfile.js
//Header.js
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right bg-white shadow-lg focus:outline-none flex flex-col rounded">
<Menu.Item as={Fragment}>
<Link to={"profile/edit"}>
<p className="px-2 py-3">Account settings</p>
</Link>
</Menu.Item>
...
</Menu.Items>
//Profile.js
<div className="ml-4">
<p className="font-bold text-xl">{author.fullName}</p>
<p>{author.bio}</p>
<p className="text-sm text-gray-400">Join Date: <span className="ml-1">{author.joinDate}</span></p>
{
currentUserId === userId &&
<Link to={`/profile/edit`}>
<button className="text-violet-600 cursor-pointer hover:text-violet-800">Edit Profile</button>
</Link>
}
</div>