From 677f126642964abf8f9a860217b961f066fc4a89 Mon Sep 17 00:00:00 2001 From: DmsAnhr Date: Thu, 29 Jan 2026 11:33:40 +0700 Subject: [PATCH] add pdf select all and animation --- .../_components/step-2-pdf-viewer.tsx | 99 ++++++++++++++++--- .../mapset-upload/_hooks/use-pdf-viewer.ts | 69 +++++++------ 2 files changed, 120 insertions(+), 48 deletions(-) diff --git a/app/(modules)/admin/mapset-upload/_components/step-2-pdf-viewer.tsx b/app/(modules)/admin/mapset-upload/_components/step-2-pdf-viewer.tsx index 415a2b6..25c923b 100644 --- a/app/(modules)/admin/mapset-upload/_components/step-2-pdf-viewer.tsx +++ b/app/(modules)/admin/mapset-upload/_components/step-2-pdf-viewer.tsx @@ -1,34 +1,85 @@ // app/admin/upload/_components/step-2-pdf-viewer.tsx "use client"; +import { useRef } from "react"; import { usePdfViewer } from "../_hooks/use-pdf-viewer"; import { Button } from "@/shared/components/ui/button"; -import { Checkbox } from "@/shared/components/ui/checkbox"; -import { Loader2 } from "lucide-react"; +import { Checkbox } from "@/shared/components/ui/checkbox"; +import { Loader2, CheckSquare, Square } from "lucide-react"; import { motion } from "framer-motion"; export default function StepPdfViewer() { - const { pages, loading, localSelectedPages, toggleSelectPage, handleProcessPdf } = usePdfViewer(); + const { + pages, + loading, + localSelectedPages, + toggleSelectPage, + handleProcessPdf, + // Fitur Baru dari Hook + handleSelectAll, + isAllSelected, + MAX_SELECT + } = usePdfViewer(); + + // 🔥 REF UNTUK SCROLLING + const pageRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); + + // Fungsi Helper Scroll + const scrollToPage = (pageNum: number) => { + const element = pageRefs.current[pageNum]; + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }; + + const handleSidebarClick = (pageNum: number) => { + toggleSelectPage(pageNum); + // Auto scroll hanya jika kita mencentang (opsional: bisa juga scroll saat uncheck jika mau) + // Disini saya buat scroll terjadi setiap kali klik baris + scrollToPage(pageNum); + }; return (
{/* Sidebar Kiri */}

Pilih Halaman

-
+
+ +

+ Maksimal {MAX_SELECT} halaman. +

+
+
{pages.map((p) => (
toggleSelectPage(p.pageNum)} + onClick={() => handleSidebarClick(p.pageNum)} > toggleSelectPage(p.pageNum)} + // onCheckedChange sudah dihandle oleh onClick parent div agar area klik lebih luas + className="pointer-events-none" /> - Halaman {p.pageNum} + + Halaman {p.pageNum} +
))}
@@ -53,16 +104,38 @@ export default function StepPdfViewer() {
)} -
+
{pages.map((p) => ( { pageRefs.current[p.pageNum] = el; }} + initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} - className="bg-white p-2 shadow rounded" + transition={{ delay: 0.05 * (p.pageNum > 10 ? 10 : p.pageNum) }} // Optimasi delay list panjang + + // Styling highlight jika terpilih + className={`bg-white p-2 shadow-sm rounded-lg border-2 transition-all duration-300 ${ + localSelectedPages.includes(p.pageNum) + ? "border-blue-500 ring-4 ring-blue-500/10" + : "border-transparent hover:border-slate-300" + }`} + onClick={() => toggleSelectPage(p.pageNum)} // Klik gambar juga bisa toggle select > - {`Page -

Halaman {p.pageNum}

+
+ {/* Badge Nomor Halaman di atas gambar */} +
+ Halaman. {p.pageNum} +
+ + {`Page +
))}
diff --git a/app/(modules)/admin/mapset-upload/_hooks/use-pdf-viewer.ts b/app/(modules)/admin/mapset-upload/_hooks/use-pdf-viewer.ts index b0d5279..252da88 100644 --- a/app/(modules)/admin/mapset-upload/_hooks/use-pdf-viewer.ts +++ b/app/(modules)/admin/mapset-upload/_hooks/use-pdf-viewer.ts @@ -199,6 +199,8 @@ export function usePdfViewer() { const [loading, setLoading] = useState(false); const [localSelectedPages, setLocalSelectedPages] = useState([]); + const MAX_SELECT = 20; + // Efek untuk memuat halaman PDF saat komponen dipasang useEffect(() => { if (state.file && state.step === "PDF_VIEWER") { @@ -210,42 +212,29 @@ export function usePdfViewer() { // --- FUNGSI RENDER PDF KE GAMBAR --- const renderPdfPages = async (pdfFile: File) => { setLoading(true); - setPages([]); // Bersihkan halaman sebelumnya - + setPages([]); try { - // Load library dari CDN const pdfjsLib = await loadPdfJs(); - - // Baca ArrayBuffer const arrayBuffer = await pdfFile.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; const pageImages = []; - // Render setiap halaman ke Canvas -> DataURL for (let pageNum = 1; pageNum <= totalPages; pageNum++) { const page = await pdf.getPage(pageNum); - const viewport = page.getViewport({ scale: 1 }); // Skala thumbnail - + const viewport = page.getViewport({ scale: 1 }); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - if (ctx) { canvas.height = viewport.height; canvas.width = viewport.width; - - await page.render({ - canvasContext: ctx, - viewport: viewport, - }).promise; - + await page.render({ canvasContext: ctx, viewport }).promise; pageImages.push({ pageNum, - imageUrl: canvas.toDataURL("image/jpeg", 0.7), // Kompresi JPEG biar ringan + imageUrl: canvas.toDataURL("image/jpeg", 0.7), }); } } - setPages(pageImages); } catch (err) { console.error("PDF Render Error:", err); @@ -261,8 +250,8 @@ export function usePdfViewer() { if (prev.includes(pageNum)) { return prev.filter((p) => p !== pageNum); } else { - if (prev.length >= 20) { - toast.warning("Maksimal 20 halaman yang dapat dipilih."); + if (prev.length >= MAX_SELECT) { + toast.warning(`Maksimal ${MAX_SELECT} halaman yang dapat dipilih.`); return prev; } return [...prev, pageNum]; @@ -270,39 +259,46 @@ export function usePdfViewer() { }); }; + // 🔥 SELECT ALL + const isAllSelected = pages.length > 0 && localSelectedPages.length === Math.min(pages.length, MAX_SELECT); + + const handleSelectAll = () => { + if (isAllSelected) { + // Jika sudah terpilih semua (atau max), batalkan semua + setLocalSelectedPages([]); + } else { + // Pilih semua (tapi batasi sesuai MAX_SELECT) + const allPageNums = pages.map(p => p.pageNum).slice(0, MAX_SELECT); + setLocalSelectedPages(allPageNums); + + if (pages.length > MAX_SELECT) { + toast.info(`Otomatis memilih ${MAX_SELECT} halaman pertama (Batas Maksimum).`); + } + } + }; + // --- LOGIKA PROSES UPLOAD & ROUTING YANG ANDA MINTA --- const handleProcessPdf = async () => { if (localSelectedPages.length === 0) { toast.warning("Harap pilih minimal 1 halaman untuk diproses."); return; } - setLoading(true); try { - // 1. Kirim ke API dengan parameter 'pages' (localSelectedPages) const res = await uploadApi.uploadFile( state.file!, - localSelectedPages, // Kirim array halaman: [1, 2, 5] - null, // sheet null karena ini PDF + localSelectedPages, + null, state.fileDesc ); - - // 2. Simpan hasil response ke context global setState(prev => ({ ...prev, result: res })); - - // 3. Cek Logic Routing - if (res.data.tables && res.data.tables.length > 0) { - // Jika ada tabel terdeteksi -> Step 3 (Table Picker) + if (res.tables && res.tables.length > 0) { goToStep("TABLE_PICKER"); - toast.success(`Ditemukan ${res.data.tables.length} tabel. Silakan pilih tabel.`); - } else if (!res.data.tables) { - // Jika TIDAK ada tabel (mungkin teks biasa/gambar) -> Step 4 (Validate/Preview Raw) + toast.success(`Ditemukan ${res.tables.length} tabel. Silakan pilih tabel.`); + } else { goToStep("VALIDATE"); toast.info("Tabel tidak terdeteksi spesifik, lanjut ke validasi."); - } else { - toast.warning(res.message); } - } catch (err: any) { console.error(err); toast.error(err.message || "Gagal memproses halaman PDF terpilih."); @@ -318,5 +314,8 @@ export function usePdfViewer() { localSelectedPages, toggleSelectPage, handleProcessPdf, + handleSelectAll, + isAllSelected, + MAX_SELECT }; } \ No newline at end of file