update
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
getPresignedUrl,
|
getPresignedUrl,
|
||||||
uploadFileToS3,
|
uploadFileToS3,
|
||||||
} from "@/service/mediaService";
|
} from "@/service/mediaService";
|
||||||
import React, { useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Lightbox from "yet-another-react-lightbox";
|
import Lightbox from "yet-another-react-lightbox";
|
||||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||||
@@ -37,6 +37,37 @@ export default function RoleUpgrade() {
|
|||||||
const [lightboxIndex, setLightboxIndex] = useState(-1);
|
const [lightboxIndex, setLightboxIndex] = useState(-1);
|
||||||
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
const handleIframeLoad = () => {
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
// Đảm bảo iframe và nội dung bên trong đã sẵn sàng và cùng nguồn gốc (same-origin)
|
||||||
|
if (!iframe || !iframe.contentDocument) return;
|
||||||
|
|
||||||
|
const updateHeight = () => {
|
||||||
|
if (iframe.contentDocument) {
|
||||||
|
// Mẹo: Reset height về 'auto' trước để lấy được chiều cao thực tế
|
||||||
|
// (đặc biệt khi người dùng xóa bớt nội dung làm chiều cao ngắn lại)
|
||||||
|
iframe.style.height = "auto";
|
||||||
|
const scrollHeight =
|
||||||
|
iframe.contentDocument.documentElement.scrollHeight;
|
||||||
|
iframe.style.height = `${scrollHeight}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Cập nhật chiều cao ngay khi iframe load xong HTML
|
||||||
|
updateHeight();
|
||||||
|
|
||||||
|
// 2. Dùng ResizeObserver để theo dõi những thay đổi sau khi load
|
||||||
|
// (VD: ảnh bên trong tải xong làm nội dung dài ra)
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (iframe.contentDocument.body) {
|
||||||
|
resizeObserver.observe(iframe.contentDocument.body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cleanHTMLContent = (rawHtml: string) => {
|
const cleanHTMLContent = (rawHtml: string) => {
|
||||||
if (!rawHtml) return "";
|
if (!rawHtml) return "";
|
||||||
@@ -198,13 +229,34 @@ export default function RoleUpgrade() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="bg-white border border-zinc-200 rounded-xl shadow-inner overflow-hidden min-h-[400px]">
|
<div className="bg-white border border-zinc-200 rounded-xl shadow-inner overflow-hidden min-h-[400px]">
|
||||||
{content ? (
|
{content ? (
|
||||||
<div className="preview-wrapper bg-white text-black">
|
<iframe
|
||||||
<div
|
ref={iframeRef}
|
||||||
dangerouslySetInnerHTML={{
|
onLoad={handleIframeLoad}
|
||||||
__html: cleanHTMLContent(content),
|
srcDoc={`
|
||||||
}}
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
color: black;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
img { max-width: 100%; height: auto; display: block; margin-top: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${cleanHTMLContent(content)}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`}
|
||||||
|
title="Preview"
|
||||||
|
className="w-full border-none flex-1 transition-all duration-300"
|
||||||
|
sandbox="allow-same-origin"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-[400px] text-zinc-400 italic bg-gray-50 dark:bg-gray-900">
|
<div className="flex flex-col items-center justify-center h-[400px] text-zinc-400 italic bg-gray-50 dark:bg-gray-900">
|
||||||
<p>Chưa có nội dung để xem trước</p>
|
<p>Chưa có nội dung để xem trước</p>
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export default function HistorianApplicationPage() {
|
|||||||
|
|
||||||
const pagination = tableData?.pagination;
|
const pagination = tableData?.pagination;
|
||||||
|
|
||||||
|
console.log(tableData)
|
||||||
// console.log("Pagination info:", pagination);
|
// console.log("Pagination info:", pagination);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export default function UserTable() {
|
|||||||
|
|
||||||
const pagination = tableData?.pagination;
|
const pagination = tableData?.pagination;
|
||||||
|
|
||||||
console.log(pagination);
|
// console.log(pagination);
|
||||||
|
|
||||||
const handleOpenDetail = (user: fullDataUser) => {
|
const handleOpenDetail = (user: fullDataUser) => {
|
||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { apiDeleteHistorianCV } from "@/service/historianService";
|
import { apiDeleteHistorianCV } from "@/service/historianService";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
// Import statusConfig để đồng bộ logic
|
|
||||||
import { statusConfig } from "@/service/handler";
|
import { statusConfig } from "@/service/handler";
|
||||||
|
|
||||||
import Lightbox from "yet-another-react-lightbox";
|
import Lightbox from "yet-another-react-lightbox";
|
||||||
@@ -17,6 +15,7 @@ import Captions from "yet-another-react-lightbox/plugins/captions";
|
|||||||
import "yet-another-react-lightbox/styles.css";
|
import "yet-another-react-lightbox/styles.css";
|
||||||
import "yet-another-react-lightbox/plugins/captions.css";
|
import "yet-another-react-lightbox/plugins/captions.css";
|
||||||
import { URL_MEDIA } from "../../../../../../api";
|
import { URL_MEDIA } from "../../../../../../api";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function ApplicationDetailPage() {
|
export default function ApplicationDetailPage() {
|
||||||
const application = useSelector(
|
const application = useSelector(
|
||||||
@@ -24,6 +23,9 @@ export default function ApplicationDetailPage() {
|
|||||||
);
|
);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [errMessage, setErrMessage] = useState<string>(
|
||||||
|
"Không thể xóa đơn đăng ký này.",
|
||||||
|
);
|
||||||
const [index, setIndex] = useState(-1);
|
const [index, setIndex] = useState(-1);
|
||||||
|
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -82,6 +84,8 @@ export default function ApplicationDetailPage() {
|
|||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
await apiDeleteHistorianCV(application.id);
|
await apiDeleteHistorianCV(application.id);
|
||||||
|
|
||||||
|
// Thành công (200 OK)
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
title: "Đã xóa!",
|
title: "Đã xóa!",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
@@ -91,15 +95,31 @@ export default function ApplicationDetailPage() {
|
|||||||
color: isDarkMode ? "#fff" : "#000",
|
color: isDarkMode ? "#fff" : "#000",
|
||||||
});
|
});
|
||||||
router.push("/profile");
|
router.push("/profile");
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
Swal.fire({ title: "Lỗi!", icon: "error" });
|
setErrMessage(
|
||||||
|
error.response?.data?.message || "Có lỗi xảy ra khi xóa!",
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
error.response?.data?.message ===
|
||||||
|
"You don't have permission to access this resource."
|
||||||
|
) {
|
||||||
|
setErrMessage("Bạn không có quyền xóa đơn đăng ký này.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Lỗi!",
|
||||||
|
text: errMessage,
|
||||||
|
icon: "error",
|
||||||
|
background: isDarkMode ? "#18181b" : "#fff",
|
||||||
|
color: isDarkMode ? "#fff" : "#000",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Application Detail:", application); // Debug log để kiểm tra dữ liệu ứng dụng
|
// console.log("Application Detail:", application);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border dark:border-zinc-800">
|
<div className="max-w-5xl mx-auto p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border dark:border-zinc-800">
|
||||||
@@ -179,7 +199,8 @@ export default function ApplicationDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{application.status !== "APPROVED" && (
|
{application.status !== "APPROVED" &&
|
||||||
|
application.status !== "REJECTED" && (
|
||||||
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
<div className="flex justify-end w-full">
|
<div className="flex justify-end w-full">
|
||||||
<button
|
<button
|
||||||
@@ -191,6 +212,56 @@ export default function ApplicationDetailPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{application?.reviewer && (
|
||||||
|
<section className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
|
<label className="text-[11px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-3 block">
|
||||||
|
Phản hồi từ người kiểm duyệt
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="p-5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-900/50">
|
||||||
|
<div className="flex items-start justify-between mb-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{application?.reviewer?.avatar_url ? (
|
||||||
|
<Image
|
||||||
|
src={application.reviewer.avatar_url}
|
||||||
|
alt={application.reviewer.display_name || "Avatar"}
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
className="w-9 h-9 rounded-full object-cover border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-9 h-9 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 font-medium text-sm">
|
||||||
|
{application?.reviewer?.display_name?.charAt(0) || "R"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||||
|
{application?.reviewer?.display_name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||||
|
{application?.reviewer?.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-zinc-400 dark:text-zinc-500 tabular-nums">
|
||||||
|
{application?.reviewed_at}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative ml-4 pl-4 border-l border-zinc-300 dark:border-zinc-700">
|
||||||
|
<div className="text-sm text-zinc-700 dark:text-zinc-300 leading-relaxed">
|
||||||
|
{application?.review_note ? (
|
||||||
|
<p>{application.review_note}</p>
|
||||||
|
) : (
|
||||||
|
<p className="italic text-zinc-500">Không có ghi chú bổ sung.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export default function Profile() {
|
|||||||
const userData = await apiGetCurrentUser();
|
const userData = await apiGetCurrentUser();
|
||||||
const mediaResponse = await apiGetCurrentUserMedia();
|
const mediaResponse = await apiGetCurrentUserMedia();
|
||||||
const userApplications = await apiGetCurrentUserApplications();
|
const userApplications = await apiGetCurrentUserApplications();
|
||||||
// console.log("User Applications:", userApplications);
|
|
||||||
|
console.log(userData);
|
||||||
|
|
||||||
if (userApplications?.data) {
|
if (userApplications?.data) {
|
||||||
setApplications(userApplications.data);
|
setApplications(userApplications.data);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { API, HOME_URL } from "../../../api";
|
import { API, HOME_URL } from "../../../api";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
export default function SignUpForm() {
|
export default function SignUpForm() {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
@@ -102,7 +103,7 @@ export default function SignUpForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const signupRes = await apiSignUp(signupPayload);
|
const signupRes = await apiSignUp(signupPayload);
|
||||||
alert("Đăng ký thành công! Đang chuyển hướng...");
|
Swal.fire("Đăng ký thành công! Đang chuyển hướng về trang đăng nhập!");
|
||||||
|
|
||||||
window.location.href = "/signin";
|
window.location.href = "/signin";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
|||||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||||
import "yet-another-react-lightbox/styles.css";
|
import "yet-another-react-lightbox/styles.css";
|
||||||
import "yet-another-react-lightbox/plugins/captions.css";
|
import "yet-another-react-lightbox/plugins/captions.css";
|
||||||
|
import { IsolatedContent } from "@/components/ui/IsolatedContent";
|
||||||
|
import { apiDeleteHistorianCV } from "@/service/historianService";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -57,7 +59,9 @@ export default function ApplicationDetailModal({
|
|||||||
const handleMediaClick = (item: any) => {
|
const handleMediaClick = (item: any) => {
|
||||||
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
||||||
if (isImageFile(item)) {
|
if (isImageFile(item)) {
|
||||||
const photoIndex = imageMediaOnly.findIndex((img: any) => img.id === item.id);
|
const photoIndex = imageMediaOnly.findIndex(
|
||||||
|
(img: any) => img.id === item.id,
|
||||||
|
);
|
||||||
setIndex(photoIndex);
|
setIndex(photoIndex);
|
||||||
} else {
|
} else {
|
||||||
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
|
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
|
||||||
@@ -87,6 +91,13 @@ export default function ApplicationDetailModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteApplication = async () => {
|
||||||
|
await apiDeleteHistorianCV(application.id);
|
||||||
|
Swal.fire("Thành công!", "Hồ sơ đã được xóa.", "success");
|
||||||
|
onRefresh();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
if (!isOpen || !application) return null;
|
if (!isOpen || !application) return null;
|
||||||
|
|
||||||
const userData = application.user || {};
|
const userData = application.user || {};
|
||||||
@@ -96,9 +107,22 @@ export default function ApplicationDetailModal({
|
|||||||
<div className="w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl dark:bg-gray-900 flex flex-col overflow-hidden text-gray-800 dark:text-gray-200">
|
<div className="w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl dark:bg-gray-900 flex flex-col overflow-hidden text-gray-800 dark:text-gray-200">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
<h3 className="text-xl font-semibold">Chi tiết yêu cầu nâng cấp</h3>
|
<h3 className="text-xl font-semibold">Chi tiết yêu cầu nâng cấp</h3>
|
||||||
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full">
|
<button
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
onClick={onClose}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,17 +130,32 @@ export default function ApplicationDetailModal({
|
|||||||
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar">
|
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar">
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="p-5 border border-gray-200 rounded-xl dark:border-gray-800 bg-gray-50/50 dark:bg-white/[0.02]">
|
<div className="p-5 border border-gray-200 rounded-xl dark:border-gray-800 bg-gray-50/50 dark:bg-white/[0.02]">
|
||||||
<h4 className="mb-4 text-xs font-bold text-gray-500 uppercase tracking-wider">Thông tin ứng viên</h4>
|
<h4 className="mb-4 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||||
|
Thông tin ứng viên
|
||||||
|
</h4>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative w-16 h-16 overflow-hidden border border-gray-200 rounded-full shrink-0">
|
<div className="relative w-16 h-16 overflow-hidden border border-gray-200 rounded-full shrink-0">
|
||||||
<Image fill src={userData.avatar_url || "/images/no-images.jpg"} alt="avatar" className="object-cover" />
|
<Image
|
||||||
|
fill
|
||||||
|
src={userData.avatar_url || "/images/no-images.jpg"}
|
||||||
|
alt="avatar"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-lg font-semibold">{userData.display_name || "N/A"}</h4>
|
<h4 className="text-lg font-semibold">
|
||||||
<p className="text-sm text-gray-500">{userData.email || "Không có email"}</p>
|
{userData.display_name || "N/A"}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{userData.email || "Không có email"}
|
||||||
|
</p>
|
||||||
<div className="mt-1 flex gap-2">
|
<div className="mt-1 flex gap-2">
|
||||||
<span className="px-2 py-0.5 text-[10px] font-bold uppercase text-blue-600 bg-blue-100 rounded-md">{application.verify_type}</span>
|
<span className="px-2 py-0.5 text-[10px] font-bold uppercase text-blue-600 bg-blue-100 rounded-md">
|
||||||
<span className={`px-2 py-0.5 text-[10px] font-semibold uppercase rounded-md ${application.status === "PENDING" ? "bg-yellow-100 text-yellow-600" : "bg-red-100 text-red-500"}`}>
|
{application.verify_type}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 text-[10px] font-semibold uppercase rounded-md ${application.status === "PENDING" ? "bg-yellow-100 text-yellow-600" : "bg-red-100 text-red-500"}`}
|
||||||
|
>
|
||||||
{application.status}
|
{application.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,15 +164,18 @@ export default function ApplicationDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Nội dung ứng tuyển</h4>
|
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||||
<div
|
Nội dung ứng tuyển
|
||||||
className="p-4 prose bg-white border border-gray-200 min-h-[100px] rounded-xl dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 max-w-none"
|
</h4>
|
||||||
dangerouslySetInnerHTML={{ __html: application.content || "<i>Không có nội dung.</i>" }}
|
<div className="p-4 prose bg-white border border-gray-200 min-h-[100px] rounded-xl dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 max-w-none">
|
||||||
/>
|
<IsolatedContent html={application.content} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Tệp đính kèm ({mediaList.length})</h4>
|
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||||
|
Tệp đính kèm ({mediaList.length})
|
||||||
|
</h4>
|
||||||
{mediaList.length > 0 ? (
|
{mediaList.length > 0 ? (
|
||||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||||
{mediaList.map((media: any, idx: number) => {
|
{mediaList.map((media: any, idx: number) => {
|
||||||
@@ -145,11 +187,20 @@ export default function ApplicationDetailModal({
|
|||||||
className="relative overflow-hidden border border-gray-200 group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-pointer"
|
className="relative overflow-hidden border border-gray-200 group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-pointer"
|
||||||
>
|
>
|
||||||
{isImg ? (
|
{isImg ? (
|
||||||
<Image src={`${URL_MEDIA}${media.storage_key}`} alt="media" fill className="object-cover transition-transform duration-300 group-hover:scale-110" />
|
<Image
|
||||||
|
src={`${URL_MEDIA}${media.storage_key}`}
|
||||||
|
alt="media"
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-300 group-hover:scale-110"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-3 text-center">
|
<div className="flex flex-col items-center justify-center w-full h-full p-3 text-center">
|
||||||
<div className="w-10 h-10 mb-2 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-lg shadow-sm text-xl">📄</div>
|
<div className="w-10 h-10 mb-2 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-lg shadow-sm text-xl">
|
||||||
<span className="text-[10px] font-medium text-gray-600 line-clamp-2">{media.original_name}</span>
|
📄
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-medium text-gray-600 line-clamp-2">
|
||||||
|
{media.original_name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/40 opacity-0 group-hover:opacity-100 z-10">
|
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/40 opacity-0 group-hover:opacity-100 z-10">
|
||||||
@@ -162,12 +213,16 @@ export default function ApplicationDetailModal({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm italic text-gray-400">Không có tệp đính kèm.</p>
|
<p className="text-sm italic text-gray-400">
|
||||||
|
Không có tệp đính kèm.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Ghi chú duyệt hồ sơ</h4>
|
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||||
|
Ghi chú duyệt hồ sơ
|
||||||
|
</h4>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="w-full p-4 text-sm bg-white border border-gray-200 rounded-xl dark:border-gray-800 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 outline-none h-[100px]"
|
className="w-full p-4 text-sm bg-white border border-gray-200 rounded-xl dark:border-gray-800 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 outline-none h-[100px]"
|
||||||
@@ -181,13 +236,36 @@ export default function ApplicationDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 border-t border-gray-200 dark:bg-gray-900/50 dark:border-gray-800">
|
<div className="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 border-t border-gray-200 dark:bg-gray-900/50 dark:border-gray-800">
|
||||||
<button onClick={onClose} className="px-5 py-2 text-sm font-medium border border-gray-300 rounded-xl hover:bg-gray-100 transition-colors">Đóng</button>
|
|
||||||
{application.status === "PENDING" && (
|
{application.status === "PENDING" && (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => handleUpdateStatus("REJECTED")} disabled={isSubmitting} className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600">Từ chối</button>
|
<button
|
||||||
<button onClick={() => handleUpdateStatus("APPROVED")} disabled={isSubmitting} className="px-5 py-2 text-sm font-medium text-white bg-green-500 rounded-xl hover:bg-green-600">Phê duyệt</button>
|
onClick={() => handleUpdateStatus("REJECTED")}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Từ chối
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleUpdateStatus("APPROVED")}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-5 py-2 text-sm font-medium text-white bg-green-500 rounded-xl hover:bg-green-600"
|
||||||
|
>
|
||||||
|
Phê duyệt
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteApplication}
|
||||||
|
className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Xóa
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2 text-sm font-medium border border-gray-300 rounded-xl hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
Đóng
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
41
src/components/ui/IsolatedContent.tsx
Normal file
41
src/components/ui/IsolatedContent.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function IsolatedContent({ html }: { html: string }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
let shadow = containerRef.current.shadowRoot;
|
||||||
|
if (!shadow) {
|
||||||
|
shadow = containerRef.current.attachShadow({ mode: "open" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleTag = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.inner-content {
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
p { margin-bottom: 1rem; }
|
||||||
|
h1, h2, h3 { margin-top: 1.5rem; margin-bottom: 0.5rem; font-weight: 700; line-height: 1.2; }
|
||||||
|
ul, ol { padding-left: 1.5rem; margin-bottom: 1rem; }
|
||||||
|
li { margin-bottom: 0.25rem; }
|
||||||
|
img { max-width: 100%; height: auto; border-radius: 8px; }
|
||||||
|
a { color: #3b82f6; text-decoration: underline; }
|
||||||
|
blockquote { border-left: 4px solid #e5e7eb; padding-left: 1rem; font-style: italic; color: #6b7280; }
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
shadow.innerHTML = `${styleTag}<div class="inner-content">${html || "<i>Không có nội dung.</i>"}</div>`;
|
||||||
|
}
|
||||||
|
}, [html]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} />;
|
||||||
|
}
|
||||||
@@ -58,6 +58,57 @@ export default function ApplicationSquareCardList({
|
|||||||
dispatch(setSelectedApplication(app));
|
dispatch(setSelectedApplication(app));
|
||||||
router.push(`/profile/applications`);
|
router.push(`/profile/applications`);
|
||||||
};
|
};
|
||||||
|
// Tạo object mapping để render icon dựa trên status
|
||||||
|
const StatusIcons: Record<string, React.ReactNode> = {
|
||||||
|
APPROVED: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
PENDING: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-6 animate-pulse p-1 "
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
REJECTED: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 border rounded-xl dark:border-zinc-800 lg:p-6 bg-white dark:bg-zinc-950">
|
<div className="p-5 border rounded-xl dark:border-zinc-800 lg:p-6 bg-white dark:bg-zinc-950">
|
||||||
@@ -68,8 +119,6 @@ export default function ApplicationSquareCardList({
|
|||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{applications?.map((app) => {
|
{applications?.map((app) => {
|
||||||
const mediaState = processMedia(app.media);
|
const mediaState = processMedia(app.media);
|
||||||
|
|
||||||
// --- LOGIC STATUS NẰM TRONG VÒNG LẶP ---
|
|
||||||
const config = statusConfig[app.status] || statusConfig.PENDING;
|
const config = statusConfig[app.status] || statusConfig.PENDING;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +127,7 @@ export default function ApplicationSquareCardList({
|
|||||||
onClick={() => handleViewDetail(app)}
|
onClick={() => handleViewDetail(app)}
|
||||||
className="group relative h-60 aspect-square border dark:border-zinc-800 rounded-xl cursor-pointer overflow-hidden transition-all duration-300 hover:ring-2 hover:ring-blue-500/50"
|
className="group relative h-60 aspect-square border dark:border-zinc-800 rounded-xl cursor-pointer overflow-hidden transition-all duration-300 hover:ring-2 hover:ring-blue-500/50"
|
||||||
>
|
>
|
||||||
{/* BACKGROUND */}
|
{/* Media Layer */}
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
{mediaState.type === "image" ? (
|
{mediaState.type === "image" ? (
|
||||||
<img
|
<img
|
||||||
@@ -106,32 +155,28 @@ export default function ApplicationSquareCardList({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OVERLAY */}
|
{/* Overlay Gradient */}
|
||||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/20 to-transparent transition-opacity group-hover:opacity-90" />
|
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/20 to-transparent transition-opacity group-hover:opacity-90" />
|
||||||
|
|
||||||
{/* TOP INFO: STATUS & FILE COUNT */}
|
{/* TOP INFO: FILE COUNT & STATUS ICON */}
|
||||||
<div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-start">
|
<div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-center">
|
||||||
<div
|
{app.media?.length > 0 ? (
|
||||||
className={`flex items-center p-1 rounded-full border ${config.container}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`w-2 h-2 rounded-full ${config.dot} ${
|
|
||||||
app.status === "PENDING" ? "animate-pulse" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{/* <span className="text-[9px] font-bold uppercase tracking-wider">
|
|
||||||
{app.status}
|
|
||||||
</span> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{app.media?.length > 0 && (
|
|
||||||
<span className="text-[9px] font-bold text-white/60 bg-black/40 px-1.5 py-0.5 rounded-md backdrop-blur-sm">
|
<span className="text-[9px] font-bold text-white/60 bg-black/40 px-1.5 py-0.5 rounded-md backdrop-blur-sm">
|
||||||
{app.media.length} FILE
|
{app.media.length} FILE
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Status Icon Wrapper */}
|
||||||
|
<div
|
||||||
|
className={`${config.container} rounded-full`}
|
||||||
|
>
|
||||||
|
{StatusIcons[app.status] || StatusIcons.PENDING}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BOTTOM INFO */}
|
{/* Bottom Content */}
|
||||||
<div className="absolute bottom-2 left-2 right-2 z-20 text-white">
|
<div className="absolute bottom-2 left-2 right-2 z-20 text-white">
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<p className="text-[10px] font-bold uppercase opacity-80 truncate">
|
<p className="text-[10px] font-bold uppercase opacity-80 truncate">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Button from "../ui/button/Button";
|
|||||||
import Input from "../form/input/InputField";
|
import Input from "../form/input/InputField";
|
||||||
import Label from "../form/Label";
|
import Label from "../form/Label";
|
||||||
import { Profile, UserMetaCardProps } from "@/interface/user";
|
import { Profile, UserMetaCardProps } from "@/interface/user";
|
||||||
import { apiUpdateUser } from "@/service/userService";
|
import { apiUpdateUser, apiUpdateUserCurrent } from "@/service/userService";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiUpdateUser(userId, formData);
|
const response = await apiUpdateUserCurrent(formData);
|
||||||
|
|
||||||
if (response && response.status === false) {
|
if (response && response.status === false) {
|
||||||
toast.error(response.message || "Cập nhật thất bại.");
|
toast.error(response.message || "Cập nhật thất bại.");
|
||||||
@@ -90,14 +90,6 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
|
<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">
|
|
||||||
Avatar
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
|
||||||
{data.data?.profile?.avatar_url || "avatar_url"}
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||||
Full Name
|
Full Name
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { UserMetaCardProps } from "@/interface/user";
|
import { UserMetaCardProps } from "@/interface/user";
|
||||||
import { uploadMedia } from "@/service/mediaService";
|
import { uploadMedia } from "@/service/mediaService";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { apiUpdateUser, apiUpdateUserCurrent } from "@/service/userService";
|
||||||
|
import { URL_MEDIA } from "../../../api";
|
||||||
|
|
||||||
export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
||||||
const currentAvatar = data.data?.profile?.avatar_url || "/images/no-images.jpg";
|
const currentAvatar =
|
||||||
|
data.data?.profile?.avatar_url || "/images/no-images.jpg";
|
||||||
const [previewImage, setPreviewImage] = useState(currentAvatar);
|
const [previewImage, setPreviewImage] = useState(currentAvatar);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -24,11 +27,12 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
const backupImage = previewImage;
|
const backupImage = previewImage;
|
||||||
setPreviewImage(objectUrl);
|
setPreviewImage(objectUrl);
|
||||||
@@ -39,11 +43,24 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
|
|
||||||
// console.log("Upload thành công:", uploadedMedia);
|
// console.log("Upload thành công:", uploadedMedia);
|
||||||
|
|
||||||
|
if (uploadedMedia?.storage_key) {
|
||||||
if (uploadedMedia?.url) {
|
const url = URL_MEDIA + uploadedMedia.storage_key;
|
||||||
setPreviewImage(uploadedMedia.url);
|
setPreviewImage(url);
|
||||||
|
if (data.data) {
|
||||||
|
try {
|
||||||
|
await apiUpdateUserCurrent({ avatar_url: url });
|
||||||
|
toast.success("Cập nhật avatar thành công!");
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Lỗi khi cập nhật avatar:", error);
|
||||||
|
toast.warning(
|
||||||
|
"Ảnh đã được tải lên nhưng không thể cập nhật hồ sơ. Vui lòng thử lại!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Lỗi khi upload avatar:", error);
|
console.error("Lỗi khi upload avatar:", error);
|
||||||
setPreviewImage(backupImage);
|
setPreviewImage(backupImage);
|
||||||
@@ -55,7 +72,6 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
<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 gap-5 xl:flex-row xl:items-center xl:justify-between">
|
||||||
@@ -75,14 +91,44 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/50 opacity-0 group-hover:opacity-100">
|
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/50 opacity-0 group-hover:opacity-100">
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<svg className="w-6 h-6 text-white animate-spin" viewBox="0 0 24 24">
|
<svg
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"></circle>
|
className="w-6 h-6 text-white animate-spin"
|
||||||
<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>
|
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>
|
||||||
) : (
|
) : (
|
||||||
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<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" />
|
className="w-6 h-6 text-white"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
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>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +148,8 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
|
|||||||
</h4>
|
</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(", ") || "No roles available"}
|
{data.data?.roles?.map((role) => role.name).join(", ") ||
|
||||||
|
"No roles available"}
|
||||||
</p>
|
</p>
|
||||||
{data.data?.profile?.bio && (
|
{data.data?.profile?.bio && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export const apiUpdateApplicationStatus = async (id: string, payload: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const apiGetUserById = async (userId: string) => {
|
export const apiGetUserById = async (userId: string) => {
|
||||||
// Thay đổi URL sao cho khớp với route Backend của bạn
|
|
||||||
const response = await api.get(API.Admin.GET_USER_BY_ID(userId));
|
const response = await api.get(API.Admin.GET_USER_BY_ID(userId));
|
||||||
return response?.data;
|
return response?.data;
|
||||||
};
|
};
|
||||||
@@ -12,8 +12,12 @@ export const apiGetCurrentUserApplications = async () => {
|
|||||||
return response?.data;
|
return response?.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const apiUpdateUser = async (id: number | string, payload: Profile) => {
|
export const apiUpdateUser = async (id:string, payload: Profile) => {
|
||||||
const response = await api.put(API.User.Update(id), payload);
|
const response = await api.put(API.User.Update(id), payload);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const apiUpdateUserCurrent = async (payload: Profile) => {
|
||||||
|
const response = await api.put(API.User.CURRENT, payload);
|
||||||
|
return response?.data;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user