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
|
// app/admin/upload/_components/step-2-pdf-viewer.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
import { usePdfViewer } from "../_hooks/use-pdf-viewer";
|
import { usePdfViewer } from "../_hooks/use-pdf-viewer";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Checkbox } from "@/shared/components/ui/checkbox";
|
import { Checkbox } from "@/shared/components/ui/checkbox";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2, CheckSquare, Square } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default function StepPdfViewer() {
|
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 (
|
return (
|
||||||
<div className="flex h-full gap-4">
|
<div className="flex h-full gap-4">
|
||||||
{/* Sidebar Kiri */}
|
{/* Sidebar Kiri */}
|
||||||
<div className="w-64 border-r pr-4 overflow-y-auto">
|
<div className="w-64 border-r pr-4 overflow-y-auto">
|
||||||
<h2 className="font-semibold mb-4">Pilih Halaman</h2>
|
<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) => (
|
{pages.map((p) => (
|
||||||
<div
|
<div
|
||||||
key={p.pageNum}
|
key={p.pageNum}
|
||||||
className={`flex items-center space-x-2 p-2 rounded cursor-pointer ${
|
className={`flex items-center space-x-3 p-2 rounded cursor-pointer border transition-colors ${
|
||||||
localSelectedPages.includes(p.pageNum) ? "bg-blue-50" : "hover:bg-gray-50"
|
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
|
<Checkbox
|
||||||
checked={localSelectedPages.includes(p.pageNum)}
|
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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,16 +104,38 @@ export default function StepPdfViewer() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
{pages.map((p) => (
|
{pages.map((p) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={p.pageNum}
|
key={p.pageNum}
|
||||||
|
// 🔥 SET REF DI SINI
|
||||||
|
ref={(el) => { pageRefs.current[p.pageNum] = el; }}
|
||||||
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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" />
|
<div className="relative">
|
||||||
<p className="text-center text-xs text-gray-500 mt-2">Halaman {p.pageNum}</p>
|
{/* 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>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,8 @@ export function usePdfViewer() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [localSelectedPages, setLocalSelectedPages] = useState<number[]>([]);
|
const [localSelectedPages, setLocalSelectedPages] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const MAX_SELECT = 20;
|
||||||
|
|
||||||
// Efek untuk memuat halaman PDF saat komponen dipasang
|
// Efek untuk memuat halaman PDF saat komponen dipasang
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.file && state.step === "PDF_VIEWER") {
|
if (state.file && state.step === "PDF_VIEWER") {
|
||||||
|
|
@ -210,42 +212,29 @@ export function usePdfViewer() {
|
||||||
// --- FUNGSI RENDER PDF KE GAMBAR ---
|
// --- FUNGSI RENDER PDF KE GAMBAR ---
|
||||||
const renderPdfPages = async (pdfFile: File) => {
|
const renderPdfPages = async (pdfFile: File) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setPages([]); // Bersihkan halaman sebelumnya
|
setPages([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load library dari CDN
|
|
||||||
const pdfjsLib = await loadPdfJs();
|
const pdfjsLib = await loadPdfJs();
|
||||||
|
|
||||||
// Baca ArrayBuffer
|
|
||||||
const arrayBuffer = await pdfFile.arrayBuffer();
|
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
const totalPages = pdf.numPages;
|
const totalPages = pdf.numPages;
|
||||||
const pageImages = [];
|
const pageImages = [];
|
||||||
|
|
||||||
// Render setiap halaman ke Canvas -> DataURL
|
|
||||||
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
||||||
const page = await pdf.getPage(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 canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||||
await page.render({
|
|
||||||
canvasContext: ctx,
|
|
||||||
viewport: viewport,
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
pageImages.push({
|
pageImages.push({
|
||||||
pageNum,
|
pageNum,
|
||||||
imageUrl: canvas.toDataURL("image/jpeg", 0.7), // Kompresi JPEG biar ringan
|
imageUrl: canvas.toDataURL("image/jpeg", 0.7),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPages(pageImages);
|
setPages(pageImages);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("PDF Render Error:", err);
|
console.error("PDF Render Error:", err);
|
||||||
|
|
@ -261,8 +250,8 @@ export function usePdfViewer() {
|
||||||
if (prev.includes(pageNum)) {
|
if (prev.includes(pageNum)) {
|
||||||
return prev.filter((p) => p !== pageNum);
|
return prev.filter((p) => p !== pageNum);
|
||||||
} else {
|
} else {
|
||||||
if (prev.length >= 20) {
|
if (prev.length >= MAX_SELECT) {
|
||||||
toast.warning("Maksimal 20 halaman yang dapat dipilih.");
|
toast.warning(`Maksimal ${MAX_SELECT} halaman yang dapat dipilih.`);
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
return [...prev, pageNum];
|
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 ---
|
// --- LOGIKA PROSES UPLOAD & ROUTING YANG ANDA MINTA ---
|
||||||
const handleProcessPdf = async () => {
|
const handleProcessPdf = async () => {
|
||||||
if (localSelectedPages.length === 0) {
|
if (localSelectedPages.length === 0) {
|
||||||
toast.warning("Harap pilih minimal 1 halaman untuk diproses.");
|
toast.warning("Harap pilih minimal 1 halaman untuk diproses.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 1. Kirim ke API dengan parameter 'pages' (localSelectedPages)
|
|
||||||
const res = await uploadApi.uploadFile(
|
const res = await uploadApi.uploadFile(
|
||||||
state.file!,
|
state.file!,
|
||||||
localSelectedPages, // Kirim array halaman: [1, 2, 5]
|
localSelectedPages,
|
||||||
null, // sheet null karena ini PDF
|
null,
|
||||||
state.fileDesc
|
state.fileDesc
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Simpan hasil response ke context global
|
|
||||||
setState(prev => ({ ...prev, result: res }));
|
setState(prev => ({ ...prev, result: res }));
|
||||||
|
if (res.tables && res.tables.length > 0) {
|
||||||
// 3. Cek Logic Routing
|
|
||||||
if (res.data.tables && res.data.tables.length > 0) {
|
|
||||||
// Jika ada tabel terdeteksi -> Step 3 (Table Picker)
|
|
||||||
goToStep("TABLE_PICKER");
|
goToStep("TABLE_PICKER");
|
||||||
toast.success(`Ditemukan ${res.data.tables.length} tabel. Silakan pilih tabel.`);
|
toast.success(`Ditemukan ${res.tables.length} tabel. Silakan pilih tabel.`);
|
||||||
} else if (!res.data.tables) {
|
} else {
|
||||||
// Jika TIDAK ada tabel (mungkin teks biasa/gambar) -> Step 4 (Validate/Preview Raw)
|
|
||||||
goToStep("VALIDATE");
|
goToStep("VALIDATE");
|
||||||
toast.info("Tabel tidak terdeteksi spesifik, lanjut ke validasi.");
|
toast.info("Tabel tidak terdeteksi spesifik, lanjut ke validasi.");
|
||||||
} else {
|
|
||||||
toast.warning(res.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error(err.message || "Gagal memproses halaman PDF terpilih.");
|
toast.error(err.message || "Gagal memproses halaman PDF terpilih.");
|
||||||
|
|
@ -318,5 +314,8 @@ export function usePdfViewer() {
|
||||||
localSelectedPages,
|
localSelectedPages,
|
||||||
toggleSelectPage,
|
toggleSelectPage,
|
||||||
handleProcessPdf,
|
handleProcessPdf,
|
||||||
|
handleSelectAll,
|
||||||
|
isAllSelected,
|
||||||
|
MAX_SELECT
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user