This commit is contained in:
2026-04-17 18:01:19 +07:00
parent 5b6b59dc01
commit 9d35fd3653
6 changed files with 467 additions and 258 deletions

View File

@@ -32,14 +32,23 @@ type PendingFile = {
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 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);
};
@@ -51,12 +60,10 @@ export default function RoleUpgrade() {
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 {
@@ -74,8 +81,8 @@ export default function RoleUpgrade() {
const newPendingFiles = await Promise.all(newFilesPromises);
setPendingFiles((prev) => [...prev, ...newPendingFiles]);
} catch (error) {
console.error("Lỗi khi chuẩn bị file đính kèm:", error);
alert("Lỗi khi chuẩn bị kết nối upload. Vui lòng thử lại!");
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 = "";
@@ -89,6 +96,11 @@ export default function RoleUpgrade() {
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);
@@ -99,29 +111,28 @@ export default function RoleUpgrade() {
});
const uploadedMediaIds = await Promise.all(uploadPromises);
const cleanPayloadContent = cleanHTMLContent(content);
const payload = {
content: content,
content: cleanPayloadContent,
media_ids: uploadedMediaIds,
verify_type: verifyType,
verify_type: verifyType,
};
console.log("Payload chuẩn bị gửi (JSON):", payload);
const cvResponse = await createHistorianCV(payload);
console.log("Response từ API:", cvResponse);
await createHistorianCV(payload);
Swal.fire({
title: "Gửi yêu cầu thành công!",
text: "Yêu cầu nâng cấp đã được gi đi. Chúng tôi sẽ xem xét và phản hồi sớm nhất có thể. Vui lòng kiểm tra Email khi có thông báo mới.",
text: "Hồ sơ của bạn đã được ghi nhận và đang chờ duyệt.",
icon: "success",
confirmButtonText: "OK",
});
setContent("");
setPendingFiles([]);
setVerifyType("ID_CARD");
setVerifyType("OTHER");
} catch (error) {
console.error("Lỗi:", error);
toast.error("Có lỗi xảy ra khi gửi yêu cầu. Vui lòng kiểm tra lại!");
console.error("Lỗi submit:", error);
toast.error("Có lỗi xảy ra khi gửi yêu cầu.");
} finally {
setIsSubmitting(false);
}
@@ -155,10 +166,53 @@ export default function RoleUpgrade() {
};
return (
<div className="max-w-4xl mx-auto">
<PageBreadcrumb pageTitle="Role Upgrade" />
<div className="max-w-4xl mx-auto pb-20">
<PageBreadcrumb pageTitle="Nâng cấp tài khoản 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">
<RichTextEditor value={content} onChange={handleContentChange} />
<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 ? (
<div className="preview-wrapper bg-white text-black">
<div
dangerouslySetInnerHTML={{
__html: cleanHTMLContent(content),
}}
/>
</div>
) : (
<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">
@@ -167,43 +221,20 @@ export default function RoleUpgrade() {
<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 focus:ring-2 focus:ring-blue-200 dark:bg-gray-800 dark:border-gray-700 transition-all cursor-pointer text-gray-700 dark:text-gray-200"
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 nhà nghiên cứu (ID Card)</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>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
* Vui lòng chọn loại tài liệu tương ng với các tệp bạn đính kèm bên dưới.
</p>
<p className="mt-2 text-xs text-red-400 dark:text-gray-400">
* Nếu bạn đính kèm nhiều loại tài liệu, vui lòng chọn mục " Tài liệu khác (Other) ".
</p>
</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">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
Tài liệu đính kèm
</h3>
<label
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border border-dashed border-blue-500 text-blue-500 hover:bg-blue-50 transition ${isSubmitting ? "opacity-50 pointer-events-none" : ""}`}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
<span>Đính kèm tệp</span>
<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
@@ -211,50 +242,41 @@ export default function RoleUpgrade() {
accept="image/*,.pdf,.doc,.docx"
onChange={handleFileChange}
/>
<span>+ Thêm tệp</span>
</label>
</div>
{pendingFiles.length > 0 && (
<div className="grid grid-cols-2 gap-4 pt-4 mt-4 border-t border-gray-100 sm:grid-cols-4 md:grid-cols-5 dark:border-gray-800">
<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}
onClick={() => handleItemClick(item)}
className="relative overflow-hidden transition-colors border border-gray-200 cursor-pointer group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:border-blue-400"
className="relative group aspect-square border rounded-xl overflow-hidden bg-gray-50"
>
{item.type === "image" ? (
<Image
src={item.previewUrl}
alt={item.name}
alt="preview"
fill
className="object-cover transition-transform group-hover:scale-110"
className="object-cover"
/>
) : (
<div
className={`absolute inset-0 flex flex-col items-center justify-center p-3 ${docStyle.bg} dark:bg-gray-800`}
className={`flex flex-col items-center justify-center h-full ${docStyle.bg} p-2`}
>
<svg
className={`w-10 h-10 ${docStyle.color} mb-2`}
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z" />
</svg>
<span className="text-xs font-medium text-center text-gray-700 break-all line-clamp-2 dark:text-gray-300">
{item.name}
<span className={`text-xs font-bold ${docStyle.color}`}>
{item.extension.toUpperCase()}
</span>
<span className="text-[10px] text-gray-500 mt-1 uppercase font-bold">
{item.extension}
<span className="text-[10px] truncate w-full text-center mt-1">
{item.name}
</span>
</div>
)}
<button
onClick={(e) => removePendingFile(item.id, e)}
className="absolute z-10 p-1 text-white transition-opacity bg-red-500 rounded-full opacity-0 top-1 right-1 group-hover:opacity-100 hover:bg-red-600"
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"
@@ -262,32 +284,9 @@ export default function RoleUpgrade() {
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{item.type === "image" && (
<div className="absolute inset-0 flex items-center justify-center transition-opacity opacity-0 pointer-events-none bg-black/40 group-hover:opacity-100">
<svg
className="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
/>
</svg>
</div>
)}
</div>
);
})}
@@ -298,26 +297,19 @@ export default function RoleUpgrade() {
<button
onClick={handleSubmit}
disabled={isSubmitting || isPreparingFiles}
className={`w-full py-4 text-white font-bold rounded-2xl shadow-lg transition-all transform flex justify-center items-center gap-2
${isSubmitting || isPreparingFiles ? "bg-gray-400 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700 active:scale-[0.98]"}`}
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 tải dữ liệu lên..." : "Gửi yêu cầu nâng cấp"}
{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]}
zoom={{ maxZoomPixelRatio: 10 }}
styles={{
root: {
zIndex: 999999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.9)",
},
}}
/>
</div>
);
}
}