Files
History-admin/src/app/(admin)/(others-pages)/(forms)/role-upgrade/page.tsx
2026-04-17 18:01:19 +07:00

316 lines
11 KiB
TypeScript

"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, { 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 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="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">
<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">
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>
);
}