This commit is contained in:
2026-04-28 09:27:54 +07:00
commit 68d05da584
320 changed files with 26229 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
import BarChartOne from "@/components/charts/bar/BarChartOne";
import ComponentCard from "@/components/common/ComponentCard";
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "Next.js Bar Chart | TailAdmin - Next.js Dashboard Template",
description:
"This is Next.js Bar Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
};
export default function page() {
return (
<div>
<PageBreadcrumb pageTitle="Bar Chart" />
<div className="space-y-6">
<ComponentCard title="Bar Chart 1">
<BarChartOne />
</ComponentCard>
</div>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import LineChartOne from "@/components/charts/line/LineChartOne";
import ComponentCard from "@/components/common/ComponentCard";
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "Next.js Line Chart | TailAdmin - Next.js Dashboard Template",
description:
"This is Next.js Line Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
};
export default function LineChart() {
return (
<div>
<PageBreadcrumb pageTitle="Line Chart" />
<div className="space-y-6">
<ComponentCard title="Line Chart 1">
<LineChartOne />
</ComponentCard>
</div>
</div>
);
}
+295
View File
@@ -0,0 +1,295 @@
"use client";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { apiDeleteHistorianCV } from "@/service/historianService";
import Swal from "sweetalert2";
import { statusConfig } from "@/service/handler";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
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 Image from "next/image";
import { URL_MEDIA } from "../../../../../api";
export default function ApplicationDetailPage() {
const application = useSelector(
(state: RootState) => state.user.selectedApplication,
);
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) {
return (
<div className="p-10 text-center text-zinc-500 font-medium">
Đang tải hoặc không dữ liệu...
</div>
);
}
const config = statusConfig[application.status] || statusConfig.PENDING;
const isImageFile = (file: any) => {
const isImageMime = file.mime_type?.startsWith("image/");
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return isImageMime || isImageExt;
};
const imageMediaOnly = (application.media || []).filter(isImageFile);
const imageSlides = imageMediaOnly.map((item: any) => ({
src: `${URL_MEDIA}${item.storage_key}`,
title: item.original_name,
description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`,
}));
const handleMediaClick = (item: any) => {
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
if (isImageFile(item)) {
const photoIndex = imageMediaOnly.findIndex(
(img: any) => img.id === item.id,
);
setIndex(photoIndex);
} else {
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
window.open(googleDocsUrl, "_blank");
}
};
const handleDelete = async () => {
const isDarkMode = document.documentElement.classList.contains("dark");
const result = await Swal.fire({
title: "Xác nhận xóa?",
text: "Bạn sẽ không thể khôi phục lại đơn đăng ký này!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#ef4444",
cancelButtonColor: "#71717a",
confirmButtonText: "Xóa ngay",
cancelButtonText: "Hủy",
background: isDarkMode ? "#18181b" : "#fff",
color: isDarkMode ? "#fff" : "#000",
});
if (result.isConfirmed) {
try {
setIsDeleting(true);
await apiDeleteHistorianCV(application.id);
// Thành công (200 OK)
await Swal.fire({
title: "Đã xóa!",
icon: "success",
timer: 1500,
showConfirmButton: false,
background: isDarkMode ? "#18181b" : "#fff",
color: isDarkMode ? "#fff" : "#000",
});
router.push("/account");
} 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);
}
}
};
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
// 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">
<div className="flex justify-between items-center mb-8 border-b dark:border-zinc-800 pb-6">
<div>
<h2 className="text-xl font-semibold dark:text-zinc-50 tracking-tight">
Chi tiết đơn đăng
</h2>
</div>
<div
className={`flex items-center gap-1.5 px-2 py-1 rounded-md backdrop-blur-md border ${config.container}`}
>
<span className={`text-[12px] font-bold uppercase tracking-wider`}>
{application.status}
</span>
</div>
</div>
<div className="space-y-10">
<section>
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
Nội dung hiển thị (CV)
</label>
<div className="rounded-2xl border-2 border-zinc-50 dark:border-zinc-800 p-6 bg-zinc-50/50 dark:bg-zinc-950/30">
<SafeHTMLRenderer html={application.content} />
</div>
</section>
{application.media && application.media.length > 0 && (
<section>
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
Tài liệu đính kèm ({application.media.length})
</label>
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
{application.media.map((item: any) => {
const isImg = isImageFile(item);
return (
<div
key={item.id}
onClick={() => handleMediaClick(item)}
className="group relative min-w-[160px] h-[160px] overflow-hidden rounded-2xl border-2 border-zinc-100 dark:border-zinc-800 cursor-pointer bg-zinc-100 dark:bg-zinc-900 flex flex-col items-center justify-center transition-all hover:border-blue-500"
>
{isImg ? (
<>
<img
src={`${URL_MEDIA}${item.storage_key}`}
alt={item.original_name}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-3">
<p className="text-[10px] text-white font-bold truncate">
Xem nh
</p>
</div>
</>
) : (
<div className="flex flex-col items-center p-4">
<div className="w-12 h-12 mb-3 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-xl shadow-inner text-2xl">
📄
</div>
<p className="text-[10px] font-black text-zinc-700 dark:text-zinc-200 uppercase tracking-tighter">
.{item.storage_key.split(".").pop()}
</p>
<p className="text-[9px] text-zinc-400 truncate w-28 mt-1 font-medium italic">
{item.original_name}
</p>
<span className="mt-3 text-[9px] bg-blue-600 text-white px-3 py-1 rounded-full font-black uppercase opacity-0 group-hover:opacity-100 transition-opacity">
Preview
</span>
</div>
)}
</div>
);
})}
</div>
</section>
)}
{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">
{formatDate(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
index={index}
open={index >= 0}
close={() => setIndex(-1)}
slides={imageSlides}
plugins={[Zoom, Captions]}
styles={{
root: {
zIndex: 99999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.95)",
},
}}
/>
</div>
);
}
+46
View File
@@ -0,0 +1,46 @@
"use client";
import AccountDetails from "@/components/user-profile/AccountDetails";
import UserInfoCard from "@/components/user-profile/UserInfoCard";
import UserMetaCard from "@/components/user-profile/UserMetaCard";
import { UserMetaCardProps } from "@/interface/user";
import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
export default function Profile() {
const [user, setUser] = useState<UserMetaCardProps | null>(null);
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
useEffect(() => {
const fetchUser = async () => {
try {
const userData = await apiGetCurrentUser();
dispatch(setUserData(userData.data));
setUser(userData);
} catch (err) {
console.error("Lỗi:", err);
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
return (
<div>
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
<h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
Profile
</h3>
<div className="space-y-6">
<UserMetaCard data={user ?? {}} />
<UserInfoCard data={{ ...user, openEdit: true }} />
<AccountDetails data={user ?? {}} />
</div>
</div>
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { useSidebar } from "@/context/SidebarContext";
import AppHeader from "@/layout/AppHeader";
import AppSidebar from "@/layout/AppSidebar";
import Backdrop from "@/layout/Backdrop";
import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice";
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
const dispatch = useDispatch()
useEffect(() => {
const fetchUser = async () => {
try {
const userData = await apiGetCurrentUser();
dispatch(setUserData(userData.data));
} catch (err) {
console.error("Lỗi:", err);
}
};
fetchUser();
}, [])
// Dynamic class for main content margin based on sidebar state
const mainContentMargin = isMobileOpen
? "ml-0"
: isExpanded || isHovered
? "lg:ml-[290px]"
: "lg:ml-[90px]";
return (
<div className="min-h-screen xl:flex">
{/* Sidebar and Backdrop */}
<AppSidebar />
<Backdrop />
{/* Main Content Area */}
<div
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
>
{/* Header */}
<AppHeader />
{/* Page Content */}
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
</div>
</div>
);
}
+74
View File
@@ -0,0 +1,74 @@
"use client";
import { useEffect, useState } from "react";
import ApplicationLibrary from "@/components/user-profile/ApplicationList";
import { MediaDto } from "@/interface/media";
import { apiGetCurrentUserApplications, apiGetCurrentUserMedia } from "@/service/userService";
import MediaLibrary from "@/components/user-profile/Media";
export default function LibraryPage() {
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
const [applications, setApplications] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchLibraryContent = async () => {
try {
const [mediaResponse, userApplications] = await Promise.all([
apiGetCurrentUserMedia(),
apiGetCurrentUserApplications()
]);
if (userApplications?.data) setApplications(userApplications.data);
setMediaData(mediaResponse);
} catch (err) {
console.error("Lỗi khi tải thư viện:", err);
} finally {
setLoading(false);
}
};
fetchLibraryContent();
}, []);
if (loading) {
return (
<div className="flex h-[50vh] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
</div>
);
}
const hasNoData = (!mediaData?.data || mediaData.data.length === 0) && applications.length === 0;
return (
<div className="min-h-screen bg-gray-50/50 p-4 dark:bg-zinc-950 lg:p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
Thư viện
</h1>
</div>
{hasNoData ? (
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-200 py-24 text-center dark:border-zinc-800">
<p className="text-lg font-medium text-gray-500 dark:text-gray-400">
Chưa nội dung nào trong thư viện
</p>
</div>
) : (
<div className="space-y-12">
{(mediaData?.data?.length ?? 0) > 0 && (
<section>
<MediaLibrary data={mediaData ?? {}} />
</section>
)}
{applications.length > 0 && (
<section>
<ApplicationLibrary applications={applications} />
</section>
)}
</div>
)}
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
import type { Metadata } from "next";
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
import React from "react";
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
import RecentOrders from "@/components/ecommerce/RecentOrders";
import DemographicCard from "@/components/ecommerce/DemographicCard";
export const metadata: Metadata = {
title:
"Admin Dashboard",
description: "This is Dashboard Home for History Web",
};
export default function Ecommerce() {
return (
<div className="grid grid-cols-12 gap-4 md:gap-6">
<div className="col-span-12 space-y-6 xl:col-span-7">
<EcommerceMetrics />
<MonthlySalesChart />
</div>
<div className="col-span-12 xl:col-span-5">
<MonthlyTarget />
</div>
<div className="col-span-12">
<StatisticsChart />
</div>
<div className="col-span-12 xl:col-span-5">
<DemographicCard />
</div>
<div className="col-span-12 xl:col-span-7">
<RecentOrders />
</div>
</div>
);
}
+362
View File
@@ -0,0 +1,362 @@
"use client";
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import RichTextEditor from "@/components/form/role-upgrade/Editor";
import {
confirmUpload,
getPresignedUrl,
uploadFileToS3,
} from "@/service/mediaService";
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";
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 { createHistorianCV } from "@/service/historianService";
import { toast } from "sonner";
import Swal from "sweetalert2";
type PendingFile = {
id: string;
file: File;
previewUrl: string;
name: string;
size: number;
type: "image" | "document";
extension: string;
presigned?: any;
};
export default function RoleUpgrade() {
const [content, setContent] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [verifyType, setVerifyType] = useState<string>("OTHER");
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
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;
if (!iframe || !iframe.contentDocument) return;
const updateHeight = () => {
if (iframe.contentDocument) {
iframe.style.height = "auto";
const scrollHeight =
iframe.contentDocument.documentElement.scrollHeight;
iframe.style.height = `${scrollHeight}px`;
}
};
updateHeight();
const resizeObserver = new ResizeObserver(() => {
updateHeight();
});
if (iframe.contentDocument.body) {
resizeObserver.observe(iframe.contentDocument.body);
}
};
const cleanHTMLContent = (rawHtml: string) => {
if (!rawHtml) return "";
const doc = new DOMParser().parseFromString(rawHtml, "text/html");
let decoded = doc.documentElement.textContent || rawHtml;
decoded = decoded.replace(/<pre[^>]*>/g, "").replace(/<\/pre>/g, "");
return decoded;
};
const handleContentChange = (value: string) => {
setContent(value);
};
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const files = event.target.files;
if (!files || files.length === 0) return;
setIsPreparingFiles(true);
try {
const newFilesPromises = Array.from(files).map(async (file) => {
const isImage = file.type.startsWith("image/");
const extension = file.name.split(".").pop()?.toLowerCase() || "";
const presigned = await getPresignedUrl(file);
return {
id: Math.random().toString(36).substring(7),
file: file,
previewUrl: isImage ? URL.createObjectURL(file) : "",
name: file.name,
size: file.size,
type: isImage ? "image" : "document",
extension: extension,
presigned: presigned,
} as PendingFile;
});
const newPendingFiles = await Promise.all(newFilesPromises);
setPendingFiles((prev) => [...prev, ...newPendingFiles]);
} catch (error) {
console.error("Lỗi khi chuẩn bị file:", error);
toast.error("Lỗi khi chuẩn bị kết nối upload.");
} finally {
setIsPreparingFiles(false);
if (event.target) event.target.value = "";
}
};
const removePendingFile = (idToRemove: string, e: React.MouseEvent) => {
e.stopPropagation();
setPendingFiles((prev) => prev.filter((item) => item.id !== idToRemove));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) {
toast.warning("Vui lòng nhập nội dung hồ sơ!");
return;
}
try {
setIsSubmitting(true);
const uploadPromises = pendingFiles.map(async (item) => {
await uploadFileToS3(item.file, item.presigned);
const confirmRes = await confirmUpload(item.presigned.token_id);
return confirmRes?.data?.id || confirmRes?.id;
});
const uploadedMediaIds = await Promise.all(uploadPromises);
const cleanPayloadContent = cleanHTMLContent(content);
const payload = {
content: cleanPayloadContent,
media_ids: uploadedMediaIds,
verify_type: verifyType,
};
await createHistorianCV(payload);
Swal.fire({
title: "Gửi yêu cầu thành công!",
text: "Hồ sơ của bạn đã được ghi nhận và đang chờ duyệt.",
icon: "success",
});
setContent("");
setPendingFiles([]);
setVerifyType("OTHER");
} catch (error) {
console.error("Lỗi submit:", error);
toast.error("Có lỗi xảy ra khi gửi yêu cầu.");
} finally {
setIsSubmitting(false);
}
};
const imageFiles = pendingFiles.filter((f) => f.type === "image");
const slides = imageFiles.map((item) => ({
src: item.previewUrl,
title: item.name,
description: `Size: ${(item.size / 1024).toFixed(2)} KB`,
}));
const handleItemClick = (item: PendingFile) => {
if (item.type === "image") {
const index = imageFiles.findIndex((img) => img.id === item.id);
setLightboxIndex(index);
} else {
const fileUrl = URL.createObjectURL(item.file);
window.open(fileUrl, "_blank");
}
};
const getDocumentStyle = (ext: string) => {
if (["pdf"].includes(ext))
return { color: "text-red-500", bg: "bg-red-50" };
if (["doc", "docx"].includes(ext))
return { color: "text-blue-500", bg: "bg-blue-50" };
if (["xls", "xlsx"].includes(ext))
return { color: "text-emerald-500", bg: "bg-emerald-50" };
return { color: "text-gray-500", bg: "bg-gray-100" };
};
return (
<div className="max-w-4xl mx-auto pb-20">
<PageBreadcrumb pageTitle="Đăng ký trở thành Nhà sử học" />
<div className="flex items-center justify-between bg-white dark:bg-gray-900 p-2 rounded-xl border border-gray-200 dark:border-gray-800 mb-6">
<div className="flex gap-2">
<button
type="button"
onClick={() => setShowPreview(false)}
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${!showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
>
Soạn thảo
</button>
<button
type="button"
onClick={() => setShowPreview(true)}
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
>
Xem trước giao diện
</button>
</div>
<span className="text-[10px] font-black uppercase text-gray-400 mr-2 tracking-widest">
{showPreview ? "Preview Mode" : "Edit Mode"}
</span>
</div>
<div className="flex flex-col gap-6">
<div className="relative min-h-[400px]">
{!showPreview ? (
<RichTextEditor value={content} onChange={handleContentChange} />
) : (
<div className="bg-white border border-zinc-200 rounded-xl shadow-inner overflow-hidden min-h-[400px]">
{content ? (
<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>
</div>
)}
</div>
)}
</div>
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
<label className="block mb-3 text-sm font-semibold text-gray-700 dark:text-white">
Loại hồ xác minh <span className="text-red-500">*</span>
</label>
<select
value={verifyType}
onChange={(e) => setVerifyType(e.target.value)}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700"
>
<option value="OTHER">Tài liệu khác (Other)</option>
<option value="ID_CARD">Thẻ nhận dạng (ID Card)</option>
<option value="EDUCATION">Bằng cấp giáo dục (Education)</option>
<option value="EXPERT">Chứng nhận chuyên gia (Expert)</option>
</select>
</div>
{/* Upload Files */}
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Tài liệu đính kèm</h3>
<label className="cursor-pointer px-4 py-2 rounded-lg border border-dashed border-blue-500 text-blue-500 hover:bg-blue-50 transition">
<input
type="file"
multiple
className="hidden"
accept="image/*,.pdf,.doc,.docx"
onChange={handleFileChange}
/>
<span>+ Thêm tệp</span>
</label>
</div>
{pendingFiles.length > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 gap-4">
{pendingFiles.map((item) => {
const docStyle = getDocumentStyle(item.extension);
return (
<div
key={item.id}
className="relative group aspect-square border rounded-xl overflow-hidden bg-gray-50"
>
{item.type === "image" ? (
<Image
src={item.previewUrl}
alt="preview"
fill
className="object-cover"
/>
) : (
<div
className={`flex flex-col items-center justify-center h-full ${docStyle.bg} p-2`}
>
<span className={`text-xs font-bold ${docStyle.color}`}>
{item.extension.toUpperCase()}
</span>
<span className="text-[10px] truncate w-full text-center mt-1">
{item.name}
</span>
</div>
)}
<button
onClick={(e) => removePendingFile(item.id, e)}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition"
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
);
})}
</div>
)}
</div>
<button
onClick={handleSubmit}
disabled={isSubmitting || isPreparingFiles}
className={`w-full py-4 text-white font-bold rounded-2xl shadow-lg transition-all ${isSubmitting || isPreparingFiles ? "bg-gray-400" : "bg-blue-600 hover:bg-blue-700"}`}
>
{isSubmitting ? "Đang xử lý..." : "Gửi yêu cầu nâng cấp"}
</button>
</div>
<Lightbox
index={lightboxIndex}
open={lightboxIndex >= 0}
close={() => setLightboxIndex(-1)}
slides={slides}
plugins={[Zoom, Captions]}
/>
</div>
);
}