add pdf select all and animation

This commit is contained in:
DmsAnhr 2026-01-29 11:33:40 +07:00
parent 26b3abeabf
commit 677f126642
2 changed files with 120 additions and 48 deletions

View File

@ -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 (
<div className="flex h-full gap-4">
{/* Sidebar Kiri */}
<div className="w-64 border-r pr-4 overflow-y-auto">
<h2 className="font-semibold mb-4">Pilih Halaman</h2>
<div className="space-y-2">
<div className="mb-3 pb-3 border-b">
<Button
variant="outline"
size="sm"
className="w-full flex justify-between"
onClick={handleSelectAll}
disabled={loading || pages.length === 0}
>
<span className="flex items-center gap-2">
{isAllSelected ? <CheckSquare className="w-4 h-4"/> : <Square className="w-4 h-4"/>}
{isAllSelected ? "Batalkan Semua" : "Pilih Semua"}
</span>
</Button>
<p className="text-xs text-slate-400 mt-2 text-center">
Maksimal {MAX_SELECT} halaman.
</p>
</div>
<div className="space-y-2 overflow-y-auto flex-1 pr-2">
{pages.map((p) => (
<div
key={p.pageNum}
className={`flex items-center space-x-2 p-2 rounded cursor-pointer ${
localSelectedPages.includes(p.pageNum) ? "bg-blue-50" : "hover:bg-gray-50"
className={`flex items-center space-x-3 p-2 rounded cursor-pointer border transition-colors ${
localSelectedPages.includes(p.pageNum)
? "bg-blue-50 border-blue-200"
: "hover:bg-slate-50 border-transparent"
}`}
onClick={() => toggleSelectPage(p.pageNum)}
onClick={() => handleSidebarClick(p.pageNum)}
>
<Checkbox
checked={localSelectedPages.includes(p.pageNum)}
onCheckedChange={() => toggleSelectPage(p.pageNum)}
// onCheckedChange sudah dihandle oleh onClick parent div agar area klik lebih luas
className="pointer-events-none"
/>
<span className="text-sm">Halaman {p.pageNum}</span>
<span className={`text-sm ${localSelectedPages.includes(p.pageNum) ? "text-blue-700 font-medium" : "text-slate-600"}`}>
Halaman {p.pageNum}
</span>
</div>
))}
</div>
@ -53,16 +104,38 @@ export default function StepPdfViewer() {
</div>
)}
<div className="space-y-6">
<div className="space-y-8">
{pages.map((p) => (
<motion.div
key={p.pageNum}
// 🔥 SET REF DI SINI
ref={(el) => { 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
>
<img src={p.imageUrl} alt={`Page ${p.pageNum}`} className="w-full h-auto" />
<p className="text-center text-xs text-gray-500 mt-2">Halaman {p.pageNum}</p>
<div className="relative">
{/* Badge Nomor Halaman di atas gambar */}
<div className="absolute top-2 left-2 bg-slate-800/80 text-white text-xs px-2 py-1 rounded backdrop-blur-sm z-10">
Halaman. {p.pageNum}
</div>
<img
src={p.imageUrl}
alt={`Page ${p.pageNum}`}
className="w-full h-auto rounded border border-slate-100"
loading="lazy"
/>
</div>
</motion.div>
))}
</div>

View File

@ -199,6 +199,8 @@ export function usePdfViewer() {
const [loading, setLoading] = useState(false);
const [localSelectedPages, setLocalSelectedPages] = useState<number[]>([]);
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
};
}