add pdf viewer
This commit is contained in:
parent
e7098a1354
commit
eba774013e
|
|
@ -31,14 +31,27 @@ export function useUploadController() {
|
||||||
const ext = f.name.split(".").pop().toLowerCase();
|
const ext = f.name.split(".").pop().toLowerCase();
|
||||||
|
|
||||||
if (ext === "pdf") {
|
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 {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const typedArray = new Uint8Array(e.target.result);
|
const typedArray = new Uint8Array(e.target.result);
|
||||||
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
|
||||||
// setPdfPageCount(pdf.numPages);
|
|
||||||
dispatch(setPdfPageCount(pdf.numPages));
|
dispatch(setPdfPageCount(pdf.numPages));
|
||||||
console.log(`📄 PDF terdeteksi dengan ${pdf.numPages} halaman`);
|
console.log(`📄 PDF terdeteksi dengan ${pdf.numPages} halaman`);
|
||||||
|
navigate("/admin/upload/pdf"); // 👈 otomatis pindah ke viewer
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(f);
|
reader.readAsArrayBuffer(f);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
92
src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx
Normal file
92
src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
93
src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx
Normal file
93
src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -80,11 +80,11 @@ export default function ViewsAdminUploadStep1() {
|
||||||
{/* {!file && (
|
{/* {!file && (
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{file && (
|
{file && ext!= 'pdf' && (
|
||||||
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
|
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
|
||||||
{/* Info File */}
|
{/* Info File */}
|
||||||
<div className="">
|
<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
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
|
|
@ -92,6 +92,8 @@ export default function ViewsAdminUploadStep1() {
|
||||||
? 'text-red-500'
|
? 'text-red-500'
|
||||||
: file.name.endsWith('.csv')
|
: file.name.endsWith('.csv')
|
||||||
? 'text-green-500'
|
? 'text-green-500'
|
||||||
|
: file.name.endsWith('.xlsx')
|
||||||
|
? 'text-green-500'
|
||||||
: file.name.endsWith('.zip')
|
: file.name.endsWith('.zip')
|
||||||
? 'text-yellow-500'
|
? 'text-yellow-500'
|
||||||
: 'text-gray-500'
|
: 'text-gray-500'
|
||||||
|
|
@ -101,7 +103,7 @@ export default function ViewsAdminUploadStep1() {
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
{ext === "pdf" && pdfPageCount && (
|
{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.
|
File PDF <span className="font-semibold">{pdfPageCount}</span> halaman.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ import AdminLayout from "../layouts/AdminLayout";
|
||||||
import ViewsAdminHome from "../pages/admin/home/views_admin_home";
|
import ViewsAdminHome from "../pages/admin/home/views_admin_home";
|
||||||
import ViewsAdminUploadStep1 from "../pages/admin/upload/views_admin_upload";
|
import ViewsAdminUploadStep1 from "../pages/admin/upload/views_admin_upload";
|
||||||
import ViewsAdminUploadValidate from "../pages/admin/upload/views_admin_validate_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 ViewsAdminUploadSuccess from "../pages/admin/upload/views_admin_success_upload";
|
||||||
import ViewsAdminPublikasi from "../pages/admin/publikasi/views_admin_publikasi";
|
import ViewsAdminPublikasi from "../pages/admin/publikasi/views_admin_publikasi";
|
||||||
import ViewsAdminUploadRules from "../pages/admin/upload/rules/views_admin_rules_upload";
|
import ViewsAdminUploadRules from "../pages/admin/upload/rules/views_admin_rules_upload";
|
||||||
|
|
@ -127,6 +128,7 @@ const router = createBrowserRouter(
|
||||||
{ path: "home", element: <ViewsAdminHome /> },
|
{ path: "home", element: <ViewsAdminHome /> },
|
||||||
{ path: "upload", element: <ViewsAdminUploadStep1 /> },
|
{ path: "upload", element: <ViewsAdminUploadStep1 /> },
|
||||||
{ path: "upload/validate", element: <ViewsAdminUploadValidate /> },
|
{ path: "upload/validate", element: <ViewsAdminUploadValidate /> },
|
||||||
|
{ path: "upload/pdf", element: <ViewsAdminPdfViewer /> },
|
||||||
{ path: "upload/success", element: <ViewsAdminUploadSuccess /> },
|
{ path: "upload/success", element: <ViewsAdminUploadSuccess /> },
|
||||||
{ path: "upload/rules", element: <ViewsAdminUploadRules /> },
|
{ path: "upload/rules", element: <ViewsAdminUploadRules /> },
|
||||||
{ path: "publikasi", element: <ViewsAdminPublikasi /> },
|
{ path: "publikasi", element: <ViewsAdminPublikasi /> },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user