Merge branch 'master' of github.com:Pregnant-Guild/FE_User_history_web

This commit is contained in:
taDuc
2026-06-01 12:24:12 +07:00
6 changed files with 747 additions and 163 deletions
+11 -14
View File
@@ -5,37 +5,35 @@ import { useState } from 'react';
const faqData = [ const faqData = [
{ {
id: 1, id: 1,
question: "1. Phần mềm này tương thích với những hệ điều hành nào?", question: "1. Tôi có thể đổi mật khẩu ở đâu?",
answer: "Hệ thống tương thích hoàn toàn với Windows 10/11, macOS 12 trở lên. Đối với môi trường máy chủ, chúng tôi hỗ trợ các bản phân phối Linux phổ biến như Ubuntu và Debian." answer: "Bạn có thể đổi mật khẩu ở mục Thông tin tài khoản ở góc dưới bên trái màn hình. Kéo xuống mục thay đổi mật khẩu và cập nhật thông tin."
}, },
{ {
id: 2, id: 2,
question: "2. Sự khác biệt giữa phiên bản Miễn phí và Trả phí là gì?", question: "2. Làm sao để liên hệ với ban quản trị khi cần hỗ trợ gấp?",
answer: "Phiên bản trả phí cung cấp băng thông không giới hạn, hỗ trợ kỹ thuật ưu tiên 24/7, và quyền truy cập sớm vào các tính năng nâng cao. Bản miễn phí sẽ giới hạn một số tính năng xuất dữ liệu." answer: "Bạn có thể liên hệ với ban quản trị qua email hoặc số điện thoại được cung cấp trong mục Liên hệ hỗ trợ"
}, },
{ {
id: 3, id: 3,
question: "3. Hệ thống hỗ trợ những phương thức kết nối nào?", question: "3. Để có thể làm kiểm duyệt viên tôi cần làm gì?",
answer: "Chúng tôi hỗ trợ kết nối qua REST API, WebSocket cho dữ liệu thời gian thực và cung cấp sẵn SDK cho các ngôn ngữ phổ biến như TypeScript, Go." answer: "Bạn có thể gửi yêu cầu làm Nhà sử học bằng cách truy cập vào mục Nhà Sử Học ở góc dưới bên trái màn hình. Điền đầy đủ thông tin yêu cầu và gửi cho chúng tôi. Chúng tôi sẽ xem xét và liên hệ qua email."
}, },
{ {
id: 4, id: 4,
question: "4. Làm thế nào để tôi có thể tích hợp vào dự án Next.js hiện tại?", question: "4. Thời gian phản hồi khi gửi yêu cầu là bao lâu?",
answer: "Bạn chỉ cần cài đặt package qua npm/yarn, thêm API Key vào file .env và gọi component Provider ở file layout.tsx gốc. Tài liệu chi tiết có sẵn trong mục Developer Docs." answer: "Thông thường chúng tôi sẽ phản hồi yêu cầu của bạn trong vòng 24-48 giờ làm việc."
}, },
{ {
id: 5, id: 5,
question: "5. Dữ liệu của tôi được bảo mật như thế nào?", question: "5. Làm gì nếu tài khoản của tôi bị khóa?",
answer: "Toàn bộ dữ liệu được mã hóa đầu cuối (End-to-End Encryption). Chúng tôi tuân thủ nghiêm ngặt các tiêu chuẩn bảo mật quốc tế và thường xuyên rà soát hệ thống để phòng chống các lỗ hổng bảo mật." answer: "Bạn có thể liên hệ với ban quản trị qua email hoặc số điện thoại được cung cấp trong mục Liên hệ hỗ trợ"
} },
]; ];
export default function Page() { export default function Page() {
// Lưu index của câu hỏi đang được mở. Mặc định mở câu đầu tiên (index 0).
const [openIndex, setOpenIndex] = useState<number | null>(0); const [openIndex, setOpenIndex] = useState<number | null>(0);
const toggleFAQ = (index: number) => { const toggleFAQ = (index: number) => {
// Nếu click lại vào câu đang mở thì đóng nó, ngược lại thì mở câu mới
setOpenIndex(openIndex === index ? null : index); setOpenIndex(openIndex === index ? null : index);
}; };
@@ -62,7 +60,6 @@ export default function Page() {
</span> </span>
</button> </button>
{/* Phần nội dung có hiệu ứng trượt */}
<div <div
className={`overflow-hidden transition-all duration-300 ease-in-out ${ className={`overflow-hidden transition-all duration-300 ease-in-out ${
isOpen ? 'max-h-96 opacity-100 pb-6' : 'max-h-0 opacity-0' isOpen ? 'max-h-96 opacity-100 pb-6' : 'max-h-0 opacity-0'
+98 -14
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import StickyHeader from "@/components/ui/StickyHeader";
import AccountDetails from "@/components/user-profile/AccountDetails"; import AccountDetails from "@/components/user-profile/AccountDetails";
import UserInfoCard from "@/components/user-profile/UserInfoCard"; import UserInfoCard from "@/components/user-profile/UserInfoCard";
import UserMetaCard from "@/components/user-profile/UserMetaCard"; import UserMetaCard from "@/components/user-profile/UserMetaCard";
@@ -8,36 +7,121 @@ import { UserMetaCardProps } from "@/interface/user";
import { apiGetCurrentUser } from "@/service/auth"; import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice"; import { setUserData } from "@/store/features/userSlice";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
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() { export default function Profile() {
const [user, setUser] = useState<UserMetaCardProps | null>(null); const currentUser = useSelector((state: RootState) => state.user.data);
const [loading, setLoading] = useState(true);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [application, setApplication] = useState<any>(null);
const [appLoading, setAppLoading] = useState(false);
const isHistorian = !!currentUser?.roles?.some(
(role: any) => role.name === "HISTORIAN"
);
// Background refresh of user data to ensure eventual consistency
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
const userData = await apiGetCurrentUser(); const userData = await apiGetCurrentUser();
dispatch(setUserData(userData.data)); dispatch(setUserData(userData.data));
setUser(userData);
} catch (err) { } catch (err) {
console.error("Lỗi:", err); console.error("Lỗi:", err);
} finally {
setLoading(false);
} }
}; };
fetchUser(); fetchUser();
}, []); }, [dispatch]);
// Fetch applications in parallel immediately if user is a historian
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 (!currentUser) {
return <Loading />;
}
const userMetaProps: UserMetaCardProps = {
data: currentUser
? {
id: currentUser.id,
email: currentUser.email,
profile: currentUser.profile,
roles: currentUser.roles?.map((role) => ({
id: Number(role.id) || undefined,
name: role.name,
})),
}
: undefined,
status: true,
};
// 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 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={userMetaProps} />
<UserInfoCard data={{ ...userMetaProps, openEdit: true }} />
<AccountDetails data={userMetaProps} />
</div>
<div className="flex-1 min-w-0 w-full">
{appLoading ? (
<div className="flex items-center justify-center p-20 w-full bg-zinc-50/50 dark:bg-zinc-950/30 rounded-2xl border border-zinc-200 dark:border-zinc-800">
<div className="w-8 h-8 border-4 border-zinc-200 border-t-blue-600 rounded-full animate-spin"></div>
</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 ( return (
<div> <div>
<StickyHeader header={`Thông tin tài khoản`} /> <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]">
<div className="md:px-12 flex mx-auto "> <h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
<div className="xl:max-w-82 pr-4 border-r border-gray-300 md:max-w-72"> Thông tin tài khoản
<UserMetaCard data={user ?? {}} /> </h3>
<UserInfoCard data={{ ...user, openEdit: true }} /> <div className="space-y-6">
<AccountDetails data={user ?? {}} /> <UserMetaCard data={userMetaProps} />
<UserInfoCard data={{ ...userMetaProps, openEdit: true }} />
<AccountDetails data={userMetaProps} />
</div> </div>
</div> </div>
</div> </div>
+26 -28
View File
@@ -143,7 +143,6 @@ export default function ProjectDetailsPage() {
}; };
const handleRemoveMember = async (userId: string) => { const handleRemoveMember = async (userId: string) => {
// 1. Hiển thị hộp thoại xác nhận bằng SweetAlert2
const result = await Swal.fire({ const result = await Swal.fire({
title: "Xác nhận xóa?", title: "Xác nhận xóa?",
text: "Bạn có chắc chắn muốn xóa thành viên này khỏi dự án?", text: "Bạn có chắc chắn muốn xóa thành viên này khỏi dự án?",
@@ -263,18 +262,18 @@ export default function ProjectDetailsPage() {
{[ {[
{ {
id: "overview", id: "overview",
label: "Overview", label: "Tổng quan",
icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
}, },
{ {
id: "members", id: "members",
label: `Members`, label: `Thành viên`,
count: project.members?.length || 0, count: project.members?.length || 0,
icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z", icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
}, },
{ {
id: "settings", id: "settings",
label: "Settings", label: "Cài đặt",
icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z", icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z",
}, },
].map((tab) => ( ].map((tab) => (
@@ -324,7 +323,7 @@ export default function ProjectDetailsPage() {
<div className="md:col-span-3"> <div className="md:col-span-3">
<div className="border border-gray-200 dark:border-[#30363d] rounded-xl overflow-hidden "> <div className="border border-gray-200 dark:border-[#30363d] rounded-xl overflow-hidden ">
<div className="bg-gray-50 dark:bg-[#161b22] px-5 py-3 border-b border-gray-200 dark:border-[#30363d] font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]"> <div className="bg-gray-50 dark:bg-[#161b22] px-5 py-3 border-b border-gray-200 dark:border-[#30363d] font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
About Thông tin
</div> </div>
<div className="p-6 bg-white dark:bg-[#0d1117] text-[15px] leading-relaxed text-gray-700 dark:text-[#8b949e]"> <div className="p-6 bg-white dark:bg-[#0d1117] text-[15px] leading-relaxed text-gray-700 dark:text-[#8b949e]">
{project.description || ( {project.description || (
@@ -339,7 +338,7 @@ export default function ProjectDetailsPage() {
<div className="md:col-span-1 space-y-6"> <div className="md:col-span-1 space-y-6">
<div> <div>
<h3 className="font-semibold text-sm mb-4 border-b border-gray-200 dark:border-[#30363d] pb-2 text-gray-800 dark:text-[#c9d1d9]"> <h3 className="font-semibold text-sm mb-4 border-b border-gray-200 dark:border-[#30363d] pb-2 text-gray-800 dark:text-[#c9d1d9]">
Owner Chủ dự án
</h3> </h3>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 shrink-0 flex items-center justify-center"> <div className="w-8 h-8 shrink-0 flex items-center justify-center">
@@ -379,7 +378,7 @@ export default function ProjectDetailsPage() {
{activeTab === "members" && ( {activeTab === "members" && (
<div className="max-w-4xl"> <div className="max-w-4xl">
<h2 className="text-2xl font-normal mb-6 pb-2 border-b border-gray-200 dark:border-[#30363d]"> <h2 className="text-2xl font-normal mb-6 pb-2 border-b border-gray-200 dark:border-[#30363d]">
Manage access Quản quyền truy cập
</h2> </h2>
<form <form
@@ -403,14 +402,14 @@ export default function ProjectDetailsPage() {
} }
className="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer" className="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
> >
<option value="EDITOR">Editor</option> <option value="EDITOR">Chỉnh sửa</option>
<option value="VIEWER">Viewer</option> <option value="VIEWER">Xem</option>
</select> </select>
<button <button
type="submit" type="submit"
className="px-5 py-2 text-sm font-medium text-white bg-[#238636] border border-transparent rounded-md hover:bg-[#2ea043] transition-colors " className="px-5 py-2 text-sm font-medium text-white bg-[#238636] border border-transparent rounded-md hover:bg-[#2ea043] transition-colors "
> >
Add member Thêm thành viên
</button> </button>
</div> </div>
</form> </form>
@@ -449,8 +448,8 @@ export default function ProjectDetailsPage() {
} }
className="text-sm px-3 py-1.5 rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#21262d] outline-none hover:bg-gray-50 dark:hover:bg-[#30363d] cursor-pointer transition-colors" className="text-sm px-3 py-1.5 rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#21262d] outline-none hover:bg-gray-50 dark:hover:bg-[#30363d] cursor-pointer transition-colors"
> >
<option value="EDITOR">Editor</option> <option value="EDITOR">Chỉnh sửa</option>
<option value="VIEWER">Viewer</option> <option value="VIEWER">Xem</option>
</select> </select>
<button <button
onClick={() => handleRemoveMember(member.user_id)} onClick={() => handleRemoveMember(member.user_id)}
@@ -488,12 +487,12 @@ export default function ProjectDetailsPage() {
<div className="max-w-3xl space-y-10"> <div className="max-w-3xl space-y-10">
<section> <section>
<h2 className="text-2xl font-normal mb-4 pb-2 border-b border-gray-200 dark:border-[#30363d]"> <h2 className="text-2xl font-normal mb-4 pb-2 border-b border-gray-200 dark:border-[#30363d]">
General Chung
</h2> </h2>
<form onSubmit={handleUpdateInfo} className="space-y-5"> <form onSubmit={handleUpdateInfo} className="space-y-5">
<div> <div>
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]"> <label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
Project name Tên dự án
</label> </label>
<input <input
type="text" type="text"
@@ -506,7 +505,7 @@ export default function ProjectDetailsPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]"> <label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
Description tả
</label> </label>
<textarea <textarea
rows={4} rows={4}
@@ -519,7 +518,7 @@ export default function ProjectDetailsPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]"> <label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
Status Nhãn
</label> </label>
<select <select
value={editForm.status} value={editForm.status}
@@ -531,32 +530,32 @@ export default function ProjectDetailsPage() {
} }
className="w-48 px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer" className="w-48 px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
> >
<option value="PUBLIC">Public</option> <option value="PUBLIC">Công khai</option>
<option value="PRIVATE">Private</option> <option value="PRIVATE">Riêng </option>
<option value="ARCHIVE">Archive</option> <option value="ARCHIVE">Lưu trữ</option>
</select> </select>
</div> </div>
<button <button
type="submit" type="submit"
className="px-5 py-2 text-sm font-medium rounded-md bg-[#21262d] text-[#c9d1d9] border border-[#30363d] hover:bg-[#30363d] transition-colors " className="px-5 py-2 text-sm font-medium rounded-md bg-[#21262d] text-[#c9d1d9] border border-[#30363d] hover:bg-[#30363d] transition-colors "
> >
Update settings Cập nhật
</button> </button>
</form> </form>
</section> </section>
<section> <section>
<h2 className="text-2xl font-normal text-red-500 mb-4 pb-2 border-b border-red-500/30"> <h2 className="text-2xl font-normal text-red-500 mb-4 pb-2 border-b border-red-500/30">
Danger Zone Vùng nguy hiểm
</h2> </h2>
<div className="border border-red-500/30 rounded-xl overflow-hidden"> <div className="border border-red-500/30 rounded-xl overflow-hidden">
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]"> <div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
Transfer ownership Chuyển quyền sở hữu
</div> </div>
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1"> <div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
Transfer this project to another member in the project. Chuyển quyền sở hữu dự án này sang một thành viên khác trong dự án.
</div> </div>
</div> </div>
<form <form
@@ -593,18 +592,17 @@ export default function ProjectDetailsPage() {
} }
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-red-500 dark:disabled:hover:bg-transparent" className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-red-500 dark:disabled:hover:bg-transparent"
> >
Transfer Chuyển quyền
</button> </button>
</form> </form>
</div> </div>
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-red-50/30 dark:hover:bg-red-900/10 transition-colors"> <div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-red-50/30 dark:hover:bg-red-900/10 transition-colors">
<div> <div>
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]"> <div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
Delete this project Xóa dự án
</div> </div>
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1"> <div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
Once you delete a project, there is no going back. Please Thận trọng với hành đng này! Xóa dự án sẽ xóa vĩnh viễn tất cả dữ liệu liên quan không thể khôi phục.
be certain.
</div> </div>
</div> </div>
<button <button
@@ -612,7 +610,7 @@ export default function ProjectDetailsPage() {
onClick={handleDeleteProject} onClick={handleDeleteProject}
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors" className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors"
> >
Delete project Xóa dự án
</button> </button>
</div> </div>
</div> </div>
+210 -24
View File
@@ -76,7 +76,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
} catch (err: any) { } catch (err: any) {
setError( 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.` || `${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( toast.error(
err?.response?.data?.message || err?.response?.data?.message ||
@@ -87,21 +87,209 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
const isPasswordMismatch = const isPasswordMismatch =
formValues.confirm_password.length > 0 && formValues.confirm_password.length > 0 &&
formValues.new_password !== formValues.confirm_password; 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">
<div className="flex flex-col gap-6">
<div>
<div className="">
<div className="flex items-center gap-2">
<MailIcon className="leading-normal text-gray-500"/>
<p className="text-sm font-medium text-gray-600 dark:text-white/90">
{data?.data?.email || "example@mail.com"}
</p>
</div>
<div className="flex items-center gap-3 mt-2">
<LockIcon className="leading-normal text-gray-500"/>
<p className="text-sm font-medium text-gray-600 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>
</div>
</Modal>
</>
);
}
// Giao diện cũ khi không phải HISTORIAN
return ( return (
<> <>
<div className="border-t border-red-200 py-6"> <div className="p-5 border border-red-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div> <div>
<div className=""> <h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
<div className="flex items-center gap-2"> Chi tiết tài khoản
<MailIcon className="leading-normal text-gray-500"/> </h4>
<p className="text-sm font-medium text-gray-600 dark:text-white/90"> <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"} {data?.data?.email || "example@mail.com"}
</p> </p>
</div> </div>
<div className="flex items-center gap-3 mt-2"> <div>
<LockIcon className="leading-normal text-gray-500"/> <p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
<p className="text-sm font-medium text-gray-600 dark:text-white/90"> Mật khẩu
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
</p> </p>
</div> </div>
@@ -120,7 +308,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" /> <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> </svg>
Edit Chỉnh sửa
</button> </button>
</div> </div>
</div> </div>
@@ -130,10 +318,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="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"> <div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90"> <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> </h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400"> <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> </p>
</div> </div>
@@ -142,7 +330,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
{/* 1. Email (Disabled) */} {/* 1. Email (Disabled) */}
<div className="col-span-2"> <div className="col-span-2">
<Label>Email Address</Label> <Label>Đa chỉ Email</Label>
<Input <Input
type="email" type="email"
defaultValue={data?.data?.email || ""} defaultValue={data?.data?.email || ""}
@@ -152,12 +340,12 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2 relative"> <div className="col-span-2 relative">
<Label>Current Password</Label> <Label>Mật khẩu hiện tại</Label>
<div className="relative"> <div className="relative">
<Input <Input
type={showOldPass ? "text" : "password"} type={showOldPass ? "text" : "password"}
name="old_password" name="old_password"
placeholder="Enter current password" placeholder="Nhập mật khẩu hiện tại"
defaultValue={formValues.old_password} defaultValue={formValues.old_password}
onChange={handleChange} onChange={handleChange}
/> />
@@ -176,12 +364,12 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2 relative"> <div className="col-span-2 relative">
<Label>New Password</Label> <Label>Mật khẩu mới</Label>
<div className="relative"> <div className="relative">
<Input <Input
type={showNewPass ? "text" : "password"} type={showNewPass ? "text" : "password"}
name="new_password" name="new_password"
placeholder="Enter new password" placeholder="Nhập mật khẩu mới"
defaultValue={formValues.new_password} defaultValue={formValues.new_password}
onChange={handleChange} onChange={handleChange}
/> />
@@ -199,15 +387,14 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
</div> </div>
</div> </div>
<div className="col-span-2 relative"> <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"> <div className="relative">
<Input <Input
type={showConfirmPass ? "text" : "password"} type={showConfirmPass ? "text" : "password"}
name="confirm_password" name="confirm_password"
placeholder="Confirm new password" placeholder="Xác nhận mật khẩu mới"
defaultValue={formValues.confirm_password} defaultValue={formValues.confirm_password}
onChange={handleChange} onChange={handleChange}
// Thêm class viền đỏ ở đây
className={ className={
isPasswordMismatch isPasswordMismatch
? "border-red-500 focus:border-red-500 dark:border-red-500" ? "border-red-500 focus:border-red-500 dark:border-red-500"
@@ -226,7 +413,6 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
)} )}
</button> </button>
</div> </div>
{/* Thêm một dòng text báo lỗi nhỏ ngay dưới ô input nếu muốn */}
{isPasswordMismatch && ( {isPasswordMismatch && (
<p className="mt-1 text-xs text-red-500"> <p className="mt-1 text-xs text-red-500">
Mật khẩu không khớp! Mật khẩu không khớp!
@@ -245,14 +431,14 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
type="button" type="button"
onClick={closeModal} onClick={closeModal}
> >
Close Đóng
</Button> </Button>
<Button <Button
size="sm" size="sm"
type="submit" type="submit"
className="bg-red-500 hover:bg-red-600 " className="bg-red-500 hover:bg-red-600 "
> >
Save Changes Lưu thay đi
</Button> </Button>
</div> </div>
</form> </form>
+275 -62
View File
@@ -85,13 +85,269 @@ 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">
{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-600 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>
<div className=" gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div className="flex items-center gap-2 py-2">
<MailIcon className="leading-normal text-gray-500" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.email || "Email"}
</p>
</div>
{data.data?.profile?.phone && (
<div className="flex items-center gap-2 py-2">
<PhoneIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.phone || "+XXX XXX XXX"}
</p>
</div>
)}
{data.data?.profile?.location && (
<div className="flex items-center gap-2 py-2">
<LocationIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.location || "Chưa cập nhật địa chỉ"}
</p>
</div>
)}
{data.data?.profile?.website && (
<div className="flex items-center gap-2 py-2">
<LinkIcon className="w-5 h-5 opacity-60" />
<Link
href={data.data?.profile?.website || "#"}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-600 hover:text-blue-500 dark:hover:text-blue-400"
>
{data.data?.profile?.website || "Chưa cập nhật website"}
</Link>
</div>
)}
</div>
</div>
</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-600 ">
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">
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>
</div>
</Modal>
</div>
);
}
// Giao diện cũ khi không phải HISTORIAN
return ( return (
<div className="py-6"> <div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-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 && ( {data.openEdit && (
<button <button
onClick={openModal} 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-600 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" 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 <svg
className="fill-current" className="fill-current"
@@ -106,74 +362,31 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
fill="" fill=""
/> />
</svg> </svg>
Edit Chỉnh sửa
</button> </button>
)} )}
<div>
<div className=" gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div className="flex items-center gap-2 py-2">
<MailIcon className="leading-normal text-gray-500" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.email || "Email address"}
</p>
</div>
{data.data?.profile?.phone && (
<div className="flex items-center gap-2 py-2">
<PhoneIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.phone || "+XXX XXX XXX"}
</p>
</div>
)}
{data.data?.profile?.location && (
<div className="flex items-center gap-2 py-2">
<LocationIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.location || "No location available"}
</p>
</div>
)}
{data.data?.profile?.website && (
<div className="flex items-center gap-2 py-2">
<LinkIcon className="w-5 h-5 opacity-60" />
<Link
href={data.data?.profile?.website || "#"}
target="_blank"
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"}
</Link>
</div>
)}
</div>
</div>
</div> </div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4"> <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="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"> <div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-600 "> <h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Edit Personal Information Chỉnh sửa thông tin nhân
</h4> </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> </div>
<form className="flex flex-col" onSubmit={handleSave}> <form className="flex flex-col" onSubmit={handleSave}>
<div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3"> <div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3">
<div className="mt-7"> <div className="mt-7">
<h5 className="mb-5 text-lg font-medium text-gray-600 lg:mb-6"> <h5 className="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
Personal Information Thông tin nhân
</h5> </h5>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div className="col-span-2"> <div className="col-span-2">
<Label>Avatar</Label> <Label>nh đi diện</Label>
<Input <Input
type="text" type="text"
name="avatar_url" name="avatar_url"
@@ -183,7 +396,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
/> />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label>Display Name</Label> <Label>Tên hiển thị</Label>
<Input <Input
type="text" type="text"
name="display_name" name="display_name"
@@ -193,7 +406,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2 lg:col-span-1"> <div className="col-span-2 lg:col-span-1">
<Label>Email Address</Label> <Label>Email</Label>
<Input <Input
type="text" type="text"
disabled disabled
@@ -202,7 +415,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2 lg:col-span-1"> <div className="col-span-2 lg:col-span-1">
<Label>Phone</Label> <Label>Số điện thoại</Label>
<Input <Input
type="text" type="text"
name="phone" name="phone"
@@ -212,7 +425,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label>Location</Label> <Label>Đa chỉ</Label>
<Input <Input
type="text" type="text"
name="location" name="location"
@@ -222,7 +435,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label>Bio</Label> <Label>Tiểu sử</Label>
<Input <Input
type="text" type="text"
name="bio" name="bio"
@@ -249,10 +462,10 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
type="button" type="button"
onClick={closeModal} onClick={closeModal}
> >
Close Đóng
</Button> </Button>
<Button size="sm" type="submit"> <Button size="sm" type="submit">
Save Changes Lưu thay đi
</Button> </Button>
</div> </div>
</form> </form>
+125 -19
View File
@@ -53,7 +53,7 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
try { try {
await apiUpdateUser({ avatar_url: url }); await apiUpdateUser({ avatar_url: url });
window.location.href = window.location.pathname; 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) { } catch (error) {
console.error("Lỗi khi cập nhật avatar:", 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!"); toast.warning("Lỗi khi cập nhật ảnh đại diện. Vui lòng thử lại!");
@@ -71,17 +71,120 @@ 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 ">
<div className="flex flex-col items-center w-full gap-6">
<div
onClick={handleAvatarClick}
className="relative w-[296px] h-[296px] overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0"
>
<Image
width={296}
height={296}
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">
<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>
</div>
<h4 className="mb-2 text-2xl font-semibold text-gray-700 text-left">
{data.data?.profile?.display_name || "Họ và tên"}
</h4>
{data.data?.profile?.bio && (
<p className="text-sm text-gray-500">
{data.data.profile.bio}
</p>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
ID: <span>{data.data?.id}</span>
</p>
</div>
</div>
</div>
</div>
);
}
// Giao diện cũ khi không phải HISTORIAN
return ( return (
<div className=""> <div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-5 "> <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"> <div className="flex flex-col items-center w-full gap-6 xl:flex-row">
<div <div
onClick={handleAvatarClick} onClick={handleAvatarClick}
className="relative w-[296px] h-[296px] overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0" className="relative w-20 h-20 overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0"
> >
<Image <Image
width={296} width={80}
height={296} height={80}
src={previewImage} src={previewImage}
alt="avatar" alt="avatar"
className="object-cover w-full h-full" className="object-cover w-full h-full"
@@ -142,23 +245,26 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
</div> </div>
<div className="order-3 xl:order-2"> <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"> <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"> <p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") || {data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"} "Chưa có vai trò"}
</p> </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> </div>
<h4 className="mb-2 text-2xl font-semibold text-gray-700 text-left"> <span className="text-sm text-gray-500 dark:text-gray-400">
{data.data?.profile?.display_name || "Full Name"} ID: {data.data?.id}
</h4> </span>
{data.data?.profile?.bio && (
<p className="text-sm text-gray-500">
{data.data.profile.bio}
</p>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
ID: <span>{data.data?.id}</span>
</p>
</div> </div>
</div> </div>
</div> </div>