add pdf select all and animation
This commit is contained in:
parent
26b3abeabf
commit
677f126642
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user