add pdf viewer

This commit is contained in:
dmsanhrProject 2025-11-06 23:34:16 +07:00
parent e7098a1354
commit eba774013e
5 changed files with 206 additions and 4 deletions

View File

@ -31,14 +31,27 @@ export function useUploadController() {
const ext = f.name.split(".").pop().toLowerCase();
if (ext === "pdf") {
// try {
// const reader = new FileReader();
// reader.onload = async (e) => {
// const typedArray = new Uint8Array(e.target.result);
// const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
// // setPdfPageCount(pdf.numPages);
// dispatch(setPdfPageCount(pdf.numPages));
// console.log(`📄 PDF terdeteksi dengan ${pdf.numPages} halaman`);
// };
// reader.readAsArrayBuffer(f);
// } catch (err) {
// console.error("Gagal membaca PDF:", err);
// }
try {
const reader = new FileReader();
reader.onload = async (e) => {
const typedArray = new Uint8Array(e.target.result);
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
// setPdfPageCount(pdf.numPages);
dispatch(setPdfPageCount(pdf.numPages));
console.log(`📄 PDF terdeteksi dengan ${pdf.numPages} halaman`);
navigate("/admin/upload/pdf"); // 👈 otomatis pindah ke viewer
};
reader.readAsArrayBuffer(f);
} catch (err) {

View File

@ -0,0 +1,92 @@
import { useDispatch, useSelector } from "react-redux";
import { useState } from "react";
import * as pdfjsLib from "pdfjs-dist";
import { setSelectedPages } from "../../../../store/slices/uploadSlice";
import { uploadPdf } from "../service_admin_upload";
import { useNavigate } from "react-router-dom";
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.mjs",
import.meta.url
).toString();
export function usePdfViewerController() {
const dispatch = useDispatch();
const navigate = useNavigate();
const { file } = useSelector((state) => state.upload);
const [pages, setPages] = useState([]);
const [selectedPages, setSelectedPagesLocal] = useState([]);
const [loading, setLoading] = useState(false);
// Render PDF menjadi gambar
const loadPdfPages = async (pdfFile) => {
setLoading(true);
try {
const reader = new FileReader();
reader.onload = async (e) => {
const typedArray = new Uint8Array(e.target.result);
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
const pageImages = [];
const totalPages = pdf.numPages;
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: ctx, viewport }).promise;
const imageUrl = canvas.toDataURL();
pageImages.push({ pageNum, imageUrl });
}
setPages(pageImages);
setLoading(false);
};
reader.readAsArrayBuffer(pdfFile);
} catch (err) {
console.error("Gagal render PDF:", err);
} finally {
// setLoading(false);
}
};
// Toggle halaman yang dipilih
const toggleSelectPage = (pageNum) => {
let updated = [...selectedPages];
if (updated.includes(pageNum)) {
updated = updated.filter((p) => p !== pageNum);
} else {
if (updated.length >= 3) return;
updated.push(pageNum);
}
setSelectedPagesLocal(updated);
dispatch(setSelectedPages(updated.join(",")));
};
const handleProcessPdf = async () => {
if (selectedPages.length === 0) return;
try {
setLoading(true);
const res = await uploadPdf({ pages: selectedPages });
console.log("PDF processed:", res);
navigate("/admin/upload/validate");
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return {
file,
pages,
loading,
selectedPages,
loadPdfPages,
toggleSelectPage,
handleProcessPdf,
};
}

View File

@ -0,0 +1,93 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { usePdfViewerController } from "./controller_pdf_viewer";
import LoadingOverlay from "../../../../components/LoadingOverlay";
import { motion } from "framer-motion";
export default function ViewsAdminPdfViewer() {
const {
file,
pages,
loading,
selectedPages,
toggleSelectPage,
handleProcessPdf,
loadPdfPages,
} = usePdfViewerController();
const navigate = useNavigate();
useEffect(() => {
if (file) {
loadPdfPages(file)
}else{
navigate("/admin/upload", { replace: true });
};
}, [file]);
return (
<div className="flex h-[calc(100vh-106px)] bg-gray-100 overflow-hidden">
{/* Sidebar kiri */}
<div className="w-64 bg-white border-r border-gray-200 p-4 flex flex-col">
<h2 className="text-lg font-semibold mb-4">Daftar Halaman</h2>
<div className="flex-1 overflow-y-auto space-y-2">
{pages.map((p) => (
<label
key={p.pageNum}
className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer hover:bg-blue-50 transition ${
selectedPages.includes(p.pageNum) ? "bg-blue-100" : ""
}`}
>
<input
type="checkbox"
checked={selectedPages.includes(p.pageNum)}
onChange={() => toggleSelectPage(p.pageNum)}
/>
<span>Halaman {p.pageNum}</span>
</label>
))}
</div>
<div className="mt-4 border-t pt-3 text-sm text-gray-600">
<p>
<span className="font-medium">Dipilih:</span>{" "}
{selectedPages.length > 0
? selectedPages.join(", ")
: "Belum ada halaman"}
</p>
<p className="text-xs mt-1 text-gray-400">
Maksimal 3 halaman yang dapat dipilih.
</p>
<button
onClick={handleProcessPdf}
disabled={selectedPages.length === 0}
className="mt-3 w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition"
>
Proses Halaman
</button>
</div>
</div>
{/* Konten kanan (viewer) */}
<div className="flex-1 relative overflow-y-auto">
<LoadingOverlay show={loading} text="Merender PDF..." />
<div className="p-6 space-y-8">
{pages.map((p) => (
<motion.div
key={p.pageNum}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: p.pageNum * 0.05 }}
className="border border-gray-300 rounded-lg bg-white shadow-sm overflow-hidden"
>
<img src={p.imageUrl} alt={`Halaman ${p.pageNum}`} className="w-full" />
<p className="text-center text-sm text-gray-500 py-2 bg-gray-50">
Halaman {p.pageNum}
</p>
</motion.div>
))}
</div>
</div>
</div>
);
}

View File

@ -80,11 +80,11 @@ export default function ViewsAdminUploadStep1() {
{/* {!file && (
)} */}
{file && (
{file && ext!= 'pdf' && (
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
{/* Info File */}
<div className="">
<p className="text-gray-800 text-sm font-medium flex items-center gap-2">
<p className="text-gray-800 text-sm font-medium flex items-center gap-2 mb-1">
📎
<span
className={`${
@ -92,6 +92,8 @@ export default function ViewsAdminUploadStep1() {
? 'text-red-500'
: file.name.endsWith('.csv')
? 'text-green-500'
: file.name.endsWith('.xlsx')
? 'text-green-500'
: file.name.endsWith('.zip')
? 'text-yellow-500'
: 'text-gray-500'
@ -101,7 +103,7 @@ export default function ViewsAdminUploadStep1() {
</span>
</p>
{ext === "pdf" && pdfPageCount && (
<p className="text-gray-500 text-xs mt-1">
<p className="text-gray-500 text-xs">
File PDF <span className="font-semibold">{pdfPageCount}</span> halaman.
</p>
)}

View File

@ -102,6 +102,7 @@ import AdminLayout from "../layouts/AdminLayout";
import ViewsAdminHome from "../pages/admin/home/views_admin_home";
import ViewsAdminUploadStep1 from "../pages/admin/upload/views_admin_upload";
import ViewsAdminUploadValidate from "../pages/admin/upload/views_admin_validate_upload";
import ViewsAdminPdfViewer from "../pages/admin/upload/pdf_viewer/views_admin_pdf_viewer";
import ViewsAdminUploadSuccess from "../pages/admin/upload/views_admin_success_upload";
import ViewsAdminPublikasi from "../pages/admin/publikasi/views_admin_publikasi";
import ViewsAdminUploadRules from "../pages/admin/upload/rules/views_admin_rules_upload";
@ -127,6 +128,7 @@ const router = createBrowserRouter(
{ path: "home", element: <ViewsAdminHome /> },
{ path: "upload", element: <ViewsAdminUploadStep1 /> },
{ path: "upload/validate", element: <ViewsAdminUploadValidate /> },
{ path: "upload/pdf", element: <ViewsAdminPdfViewer /> },
{ path: "upload/success", element: <ViewsAdminUploadSuccess /> },
{ path: "upload/rules", element: <ViewsAdminUploadRules /> },
{ path: "publikasi", element: <ViewsAdminPublikasi /> },