feat: implement user account management page and password update functionality
Build and Release / release (push) Successful in 37s

This commit is contained in:
2026-05-30 23:01:15 +07:00
parent c9082a9f58
commit da4dea7f5d
4 changed files with 688 additions and 112 deletions
+75 -4
View File
@@ -1,6 +1,5 @@
"use client";
import StickyHeader from "@/components/ui/StickyHeader";
import AccountDetails from "@/components/user-profile/AccountDetails";
import UserInfoCard from "@/components/user-profile/UserInfoCard";
import UserMetaCard from "@/components/user-profile/UserMetaCard";
@@ -9,12 +8,22 @@ import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import StickyHeader from "@/components/ui/StickyHeader";
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
import { apiGetCurrentUserApplications } from "@/service/userService";
import Loading from "@/app/loading";
export default function Profile() {
const [user, setUser] = useState<UserMetaCardProps | null>(null);
const [loading, setLoading] = useState(true);
const [application, setApplication] = useState<any>(null);
const [appLoading, setAppLoading] = useState(false);
const dispatch = useDispatch();
const isHistorian = !!user?.data?.roles?.some(
(role: any) => role.name === "HISTORIAN"
);
useEffect(() => {
const fetchUser = async () => {
try {
@@ -28,13 +37,75 @@ export default function Profile() {
}
};
fetchUser();
}, []);
}, [dispatch]);
useEffect(() => {
if (isHistorian) {
const fetchApp = async () => {
try {
setAppLoading(true);
const res = await apiGetCurrentUserApplications();
if (res?.data) {
const approvedApp =
res.data.find((app: any) => app.status === "APPROVED") ||
res.data[0];
setApplication(approvedApp);
}
} catch (err) {
console.error("Lỗi khi tải hồ sơ nhà sử học:", err);
} finally {
setAppLoading(false);
}
};
fetchApp();
}
}, [isHistorian]);
if (loading) {
return (
<Loading/>
);
}
// Nếu người dùng có role là HISTORIAN
if (isHistorian) {
return (
<div>
<StickyHeader header={`Thông tin tài khoản`} />
<div className="md:px-12 flex mx-auto ">
<div className="xl:max-w-82 pr-4 border-r border-gray-300 md:max-w-72">
<div className="md:px-12 flex flex-col md:flex-row mx-auto gap-6 w-full max-w-7xl items-start">
<div className="w-full md:max-w-72 xl:max-w-82 pr-0 md:pr-4 border-b md:border-b-0 md:border-r border-gray-300 pb-6 md:pb-0 shrink-0 space-y-6">
<UserMetaCard data={user ?? {}} />
<UserInfoCard data={{ ...user, openEdit: true }} />
<AccountDetails data={user ?? {}} />
</div>
<div className="flex-1 min-w-0 w-full">
{appLoading ? (
<div>
<Loading/>
</div>
) : application ? (
<div className="">
<SafeHTMLRenderer html={application.content} />
</div>
) : (
<div className="p-10 text-center text-zinc-500 font-medium bg-zinc-50/50 dark:bg-zinc-950/30 rounded-2xl border-2 border-zinc-50 dark:border-zinc-800">
Không tìm thấy thông tin hồ nhà sử học.
</div>
)}
</div>
</div>
</div>
);
}
return (
<div>
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6 mt-[100px]">
<h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
Thông tin tài khoản
</h3>
<div className="space-y-6">
<UserMetaCard data={user ?? {}} />
<UserInfoCard data={{ ...user, openEdit: true }} />
<AccountDetails data={user ?? {}} />
+201 -15
View File
@@ -76,7 +76,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
} catch (err: any) {
setError(
`${err?.response?.data?.message}, Cập nhật thất bại, vui lòng kiểm tra lại thông tin. Mật khẩu tối thiểu 8 ký tự, 1 in hoa, 1 số và 1 ký tự đặc biệt.` ||
"Failed to update password. Please check your current password.",
"Không thể cập nhật mật khẩu. Vui lòng kiểm tra lại mật khẩu hiện tại.",
);
toast.error(
err?.response?.data?.message ||
@@ -87,6 +87,13 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
const isPasswordMismatch =
formValues.confirm_password.length > 0 &&
formValues.new_password !== formValues.confirm_password;
const isHistorian = !!data.data?.roles?.some(
(role: any) => role.name === "HISTORIAN"
);
// Giao diện khi là HISTORIAN
if (isHistorian) {
return (
<>
<div className="border-t border-red-200 py-6">
@@ -120,7 +127,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
>
<path d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z" />
</svg>
Edit
Chỉnh sửa
</button>
</div>
</div>
@@ -130,10 +137,10 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Update Password
Cập nhật mật khẩu
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
For security, please enter your current password to set a new one.
do bảo mật, vui lòng nhập mật khẩu hiện tại đ thiết lập mật khẩu mới.
</p>
</div>
@@ -142,7 +149,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
{/* 1. Email (Disabled) */}
<div className="col-span-2">
<Label>Email Address</Label>
<Label>Đa chỉ Email</Label>
<Input
type="email"
defaultValue={data?.data?.email || ""}
@@ -152,12 +159,12 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2 relative">
<Label>Current Password</Label>
<Label>Mật khẩu hiện tại</Label>
<div className="relative">
<Input
type={showOldPass ? "text" : "password"}
name="old_password"
placeholder="Enter current password"
placeholder="Nhập mật khẩu hiện tại"
defaultValue={formValues.old_password}
onChange={handleChange}
/>
@@ -176,12 +183,12 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2 relative">
<Label>New Password</Label>
<Label>Mật khẩu mới</Label>
<div className="relative">
<Input
type={showNewPass ? "text" : "password"}
name="new_password"
placeholder="Enter new password"
placeholder="Nhập mật khẩu mới"
defaultValue={formValues.new_password}
onChange={handleChange}
/>
@@ -199,15 +206,14 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div>
</div>
<div className="col-span-2 relative">
<Label>Confirm New Password</Label>
<Label>Xác nhận mật khẩu mới</Label>
<div className="relative">
<Input
type={showConfirmPass ? "text" : "password"}
name="confirm_password"
placeholder="Confirm new password"
placeholder="Xác nhận mật khẩu mới"
defaultValue={formValues.confirm_password}
onChange={handleChange}
// Thêm class viền đỏ ở đây
className={
isPasswordMismatch
? "border-red-500 focus:border-red-500 dark:border-red-500"
@@ -226,7 +232,6 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
)}
</button>
</div>
{/* Thêm một dòng text báo lỗi nhỏ ngay dưới ô input nếu muốn */}
{isPasswordMismatch && (
<p className="mt-1 text-xs text-red-500">
Mật khẩu không khớp!
@@ -245,14 +250,195 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
type="button"
onClick={closeModal}
>
Close
Đóng
</Button>
<Button
size="sm"
type="submit"
className="bg-red-500 hover:bg-red-600 "
>
Save Changes
Lưu thay đi
</Button>
</div>
</form>
</div>
</Modal>
</>
);
}
// Giao diện cũ khi không phải HISTORIAN
return (
<>
<div className="p-5 border border-red-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
Chi tiết tài khoản
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Đa chỉ Email
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data?.data?.email || "example@mail.com"}
</p>
</div>
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Mật khẩu
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
</p>
</div>
</div>
</div>
<button
onClick={openModal}
className="flex items-center justify-center gap-2 rounded-full border border-red-300 bg-white px-4 py-3 text-sm font-medium text-red-600 hover:bg-gray-50 dark:border-red-700 dark:bg-red-800 dark:text-red-400 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
>
<path d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z" />
</svg>
Chỉnh sửa
</button>
</div>
</div>
{/* Modal Chỉnh sửa */}
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Cập nhật mật khẩu
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
do bảo mật, vui lòng nhập mật khẩu hiện tại đ thiết lập mật khẩu mới.
</p>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="px-2 overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
{/* 1. Email (Disabled) */}
<div className="col-span-2">
<Label>Đa chỉ Email</Label>
<Input
type="email"
defaultValue={data?.data?.email || ""}
disabled
className="bg-gray-100 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
<div className="col-span-2 relative">
<Label>Mật khẩu hiện tại</Label>
<div className="relative">
<Input
type={showOldPass ? "text" : "password"}
name="old_password"
placeholder="Nhập mật khẩu hiện tại"
defaultValue={formValues.old_password}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setShowOldPass(!showOldPass)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showOldPass ? (
<EyeCloseIcon className="w-5 h-5" />
) : (
<EyeIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
<div className="col-span-2 relative">
<Label>Mật khẩu mới</Label>
<div className="relative">
<Input
type={showNewPass ? "text" : "password"}
name="new_password"
placeholder="Nhập mật khẩu mới"
defaultValue={formValues.new_password}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setShowNewPass(!showNewPass)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showNewPass ? (
<EyeCloseIcon className="w-5 h-5" />
) : (
<EyeIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
<div className="col-span-2 relative">
<Label>Xác nhận mật khẩu mới</Label>
<div className="relative">
<Input
type={showConfirmPass ? "text" : "password"}
name="confirm_password"
placeholder="Xác nhận mật khẩu mới"
defaultValue={formValues.confirm_password}
onChange={handleChange}
className={
isPasswordMismatch
? "border-red-500 focus:border-red-500 dark:border-red-500"
: ""
}
/>
<button
type="button"
onClick={() => setShowConfirmPass(!showConfirmPass)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showConfirmPass ? (
<EyeCloseIcon className="w-5 h-5" />
) : (
<EyeIcon className="w-5 h-5" />
)}
</button>
</div>
{isPasswordMismatch && (
<p className="mt-1 text-xs text-red-500">
Mật khẩu không khớp!
</p>
)}
</div>
</div>
{error && <p className="mt-4 text-sm text-red-500">{error}</p>}
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button
size="sm"
variant="outline"
type="button"
onClick={closeModal}
>
Đóng
</Button>
<Button
size="sm"
type="submit"
className="bg-red-500 hover:bg-red-600 "
>
Lưu thay đi
</Button>
</div>
</form>
+227 -14
View File
@@ -85,6 +85,12 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
}
};
const isHistorian = !!data.data?.roles?.some(
(role: any) => role.name === "HISTORIAN"
);
// Giao diện khi là HISTORIAN
if (isHistorian) {
return (
<div className="py-6">
<div className="flex flex-col gap-6">
@@ -106,7 +112,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
fill=""
/>
</svg>
Edit
Chỉnh sửa
</button>
)}
<div>
@@ -115,7 +121,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
<MailIcon className="leading-normal text-gray-500" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.email || "Email address"}
{data.data?.email || "Email"}
</p>
</div>
@@ -134,7 +140,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
<LocationIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.location || "No location available"}
{data.data?.profile?.location || "Chưa cập nhật địa chỉ"}
</p>
</div>
)}
@@ -149,7 +155,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
rel="noopener noreferrer"
className="text-sm font-medium text-gray-600 hover:text-blue-500 dark:hover:text-blue-400"
>
{data.data?.profile?.website || "No website"}
{data.data?.profile?.website || "Chưa cập nhật website"}
</Link>
</div>
)}
@@ -161,19 +167,19 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
<div className="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-600 ">
Edit Personal Information
Chỉnh sửa thông tin nhân
</h4>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3">
<div className="mt-7">
<h5 className="mb-5 text-lg font-medium text-gray-600 lg:mb-6">
Personal Information
Thông tin nhân
</h5>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div className="col-span-2">
<Label>Avatar</Label>
<Label>nh đi diện</Label>
<Input
type="text"
name="avatar_url"
@@ -183,7 +189,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
/>
</div>
<div className="col-span-2">
<Label>Display Name</Label>
<Label>Tên hiển thị</Label>
<Input
type="text"
name="display_name"
@@ -193,7 +199,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2 lg:col-span-1">
<Label>Email Address</Label>
<Label>Email</Label>
<Input
type="text"
disabled
@@ -202,7 +208,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2 lg:col-span-1">
<Label>Phone</Label>
<Label>Số điện thoại</Label>
<Input
type="text"
name="phone"
@@ -212,7 +218,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2">
<Label>Location</Label>
<Label>Đa chỉ</Label>
<Input
type="text"
name="location"
@@ -222,7 +228,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div>
<div className="col-span-2">
<Label>Bio</Label>
<Label>Tiểu sử</Label>
<Input
type="text"
name="bio"
@@ -249,10 +255,217 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
type="button"
onClick={closeModal}
>
Close
Đóng
</Button>
<Button size="sm" type="submit">
Save Changes
Lưu thay đi
</Button>
</div>
</form>
</div>
</Modal>
</div>
);
}
// Giao diện cũ khi không phải HISTORIAN
return (
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
Thông tin nhân
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Họ tên
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.display_name || "Họ và tên"}
</p>
</div>
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Email
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.email || "Email"}
</p>
</div>
{data.data?.profile?.phone && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Số điện thoại
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.phone || "+XXX XXX XXX"}
</p>
</div>
)}
{data.data?.profile?.bio && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Tiểu sử
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.bio || "Chưa cập nhật tiểu sử"}
</p>
</div>
)}
{data.data?.profile?.location && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Đa chỉ
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.location || "Chưa cập nhật địa chỉ"}
</p>
</div>
)}
{data.data?.profile?.website && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Website
</p>
<Link
href={data.data?.profile?.website || "#"}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-800 dark:text-white/90 hover:text-blue-500 dark:hover:text-blue-400"
>
{data.data?.profile?.website || "Chưa cập nhật website"}
</Link>
</div>
)}
</div>
</div>
{data.openEdit && (
<button
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Chỉnh sửa
</button>
)}
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Chỉnh sửa thông tin nhân
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Cập nhật thông tin chi tiết đ giữ hồ của bạn luôn mới nhất.
</p>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3">
<div className="mt-7">
<h5 className="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
Thông tin nhân
</h5>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div className="col-span-2">
<Label>nh đi diện</Label>
<Input
type="text"
name="avatar_url"
defaultValue={formData.avatar_url}
onChange={handleChange}
disabled
/>
</div>
<div className="col-span-2">
<Label>Tên hiển thị</Label>
<Input
type="text"
name="display_name"
defaultValue={formData.display_name}
onChange={handleChange}
/>
</div>
<div className="col-span-2 lg:col-span-1">
<Label>Email</Label>
<Input
type="text"
disabled
defaultValue={data.data?.email}
/>
</div>
<div className="col-span-2 lg:col-span-1">
<Label>Số điện thoại</Label>
<Input
type="text"
name="phone"
defaultValue={formData.phone}
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<Label>Đa chỉ</Label>
<Input
type="text"
name="location"
defaultValue={formData.location}
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<Label>Tiểu sử</Label>
<Input
type="text"
name="bio"
defaultValue={formData.bio}
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<Label>Website</Label>
<Input
type="text"
name="website"
defaultValue={formData.website}
onChange={handleChange}
/>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button
size="sm"
variant="outline"
type="button"
onClick={closeModal}
>
Đóng
</Button>
<Button size="sm" type="submit">
Lưu thay đi
</Button>
</div>
</form>
+109 -3
View File
@@ -53,7 +53,7 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
try {
await apiUpdateUser({ avatar_url: url });
window.location.href = window.location.pathname;
toast.success("Cập nhật avatar thành công!");
toast.success("Cập nhật ảnh đại diện thành công!");
} catch (error) {
console.error("Lỗi khi cập nhật avatar:", error);
toast.warning("Lỗi khi cập nhật ảnh đại diện. Vui lòng thử lại!");
@@ -71,6 +71,13 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
}
}
};
const isHistorian = !!data.data?.roles?.some(
(role: any) => role.name === "HISTORIAN"
);
// Giao diện khi là HISTORIAN (không có viền ngoài, avatar to 296px)
if (isHistorian) {
return (
<div className="">
<div className="flex flex-col gap-5 ">
@@ -145,11 +152,11 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
<div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"}
"Chưa có vai trò"}
</p>
</div>
<h4 className="mb-2 text-2xl font-semibold text-gray-700 text-left">
{data.data?.profile?.display_name || "Full Name"}
{data.data?.profile?.display_name || "Họ và tên"}
</h4>
{data.data?.profile?.bio && (
<p className="text-sm text-gray-500">
@@ -164,4 +171,103 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
</div>
</div>
);
}
// Giao diện cũ khi không phải HISTORIAN
return (
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-col items-center w-full gap-6 xl:flex-row">
<div
onClick={handleAvatarClick}
className="relative w-20 h-20 overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0"
>
<Image
width={80}
height={80}
src={previewImage}
alt="avatar"
className="object-cover w-full h-full"
priority
/>
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/50 opacity-0 group-hover:opacity-100">
{isUploading ? (
<svg
className="w-6 h-6 text-white animate-spin"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<svg
className="w-6 h-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
)}
</div>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileChange}
/>
</div>
<div className="order-3 xl:order-2">
<h4 className="mb-2 text-lg font-semibold text-center text-gray-800 dark:text-white/90 xl:text-left">
{data.data?.profile?.display_name || "Họ và tên"}
</h4>
<div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"Chưa có vai trò"}
</p>
{data.data?.profile?.bio && (
<>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-[450px] truncate">
{data.data.profile.bio}
</p>
</>
)}
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
ID: {data.data?.id}
</span>
</div>
</div>
</div>
</div>
);
}