This commit is contained in:
2026-04-18 18:02:21 +07:00
parent 9d35fd3653
commit 300b35190d
14 changed files with 439 additions and 107 deletions

View File

@@ -7,7 +7,7 @@ import {
getPresignedUrl,
uploadFileToS3,
} from "@/service/mediaService";
import React, { useState } from "react";
import React, { useRef, useState } from "react";
import Image from "next/image";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
@@ -37,6 +37,37 @@ export default function RoleUpgrade() {
const [lightboxIndex, setLightboxIndex] = useState(-1);
const [isPreparingFiles, setIsPreparingFiles] = 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) => {
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]">
{content ? (
<div className="preview-wrapper bg-white text-black">
<div
dangerouslySetInnerHTML={{
__html: cleanHTMLContent(content),
}}
/>
</div>
<iframe
ref={iframeRef}
onLoad={handleIframeLoad}
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 className="flex flex-col items-center justify-center h-[400px] text-zinc-400 italic bg-gray-50 dark:bg-gray-900">
<p>Chưa nội dung đ xem trước</p>

View File

@@ -186,7 +186,8 @@ export default function HistorianApplicationPage() {
};
const pagination = tableData?.pagination;
console.log(tableData)
// console.log("Pagination info:", pagination);
return (
<div>

View File

@@ -186,7 +186,7 @@ export default function UserTable() {
const pagination = tableData?.pagination;
console.log(pagination);
// console.log(pagination);
const handleOpenDetail = (user: fullDataUser) => {
setSelectedUser(user);

View File

@@ -7,8 +7,6 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { apiDeleteHistorianCV } from "@/service/historianService";
import Swal from "sweetalert2";
// Import statusConfig để đồng bộ logic
import { statusConfig } from "@/service/handler";
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/plugins/captions.css";
import { URL_MEDIA } from "../../../../../../api";
import Image from "next/image";
export default function ApplicationDetailPage() {
const application = useSelector(
@@ -24,6 +23,9 @@ export default function ApplicationDetailPage() {
);
const router = useRouter();
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);
if (!application) {
@@ -82,6 +84,8 @@ export default function ApplicationDetailPage() {
try {
setIsDeleting(true);
await apiDeleteHistorianCV(application.id);
// Thành công (200 OK)
await Swal.fire({
title: "Đã xóa!",
icon: "success",
@@ -91,15 +95,31 @@ export default function ApplicationDetailPage() {
color: isDarkMode ? "#fff" : "#000",
});
router.push("/profile");
} catch (error) {
Swal.fire({ title: "Lỗi!", icon: "error" });
} catch (error: any) {
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 {
setIsDeleting(false);
}
}
};
console.log("Application Detail:", application); // Debug log để kiểm tra dữ liệu ứng dụng
// console.log("Application Detail:", application);
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">
@@ -179,19 +199,70 @@ export default function ApplicationDetailPage() {
</section>
)}
{application.status !== "APPROVED" && (
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
<div className="flex justify-end w-full">
<button
onClick={handleDelete}
disabled={isDeleting}
className="group flex items-center gap-2 px-8 py-3 bg-red-100 hover:bg-red-500 text-red-600 hover:text-white dark:bg-red-950/20 dark:hover:bg-red-600 dark:text-red-400 dark:hover:text-white text-sm font-black rounded-xl transition-all duration-300 disabled:opacity-50"
>
{isDeleting ? "ĐANG XỬ LÝ..." : "XÓA"}
</button>
{application.status !== "APPROVED" &&
application.status !== "REJECTED" && (
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
<div className="flex justify-end w-full">
<button
onClick={handleDelete}
disabled={isDeleting}
className="group flex items-center gap-2 px-8 py-3 bg-red-100 hover:bg-red-500 text-red-600 hover:text-white dark:bg-red-950/20 dark:hover:bg-red-600 dark:text-red-400 dark:hover:text-white text-sm font-black rounded-xl transition-all duration-300 disabled:opacity-50"
>
{isDeleting ? "ĐANG XỬ LÝ..." : "XÓA"}
</button>
</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 ghi chú bổ sung.</p>
)}
</div>
</div>
</div>
</section>
)}
</div>
<Lightbox

View File

@@ -23,7 +23,8 @@ export default function Profile() {
const userData = await apiGetCurrentUser();
const mediaResponse = await apiGetCurrentUserMedia();
const userApplications = await apiGetCurrentUserApplications();
// console.log("User Applications:", userApplications);
console.log(userData);
if (userApplications?.data) {
setApplications(userApplications.data);