"use client" import { useCallback, useEffect, useRef, useState } from "react" import { ChevronDown, ChevronUp, Loader2, Printer, ZoomIn, ZoomOut } from "lucide-react" import { getDocument, GlobalWorkerOptions, type PDFDocumentLoadingTask, type PDFDocumentProxy, } from "pdfjs-dist" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url, ).toString() const PAGE_GAP_PX = 24 const MIN_ZOOM = 0.5 const MAX_ZOOM = 2.5 const ZOOM_STEP = 0.15 const RENDER_MARGIN_PX = 320 const HORIZONTAL_PADDING_PX = 48 function fitScaleForPage(pageWidth: number, containerWidth: number): number { if (pageWidth <= 0 || containerWidth <= 0) return 1 return Math.min(2, Math.max(0.25, containerWidth / pageWidth)) } async function releasePdfDocument(doc: PDFDocumentProxy | null) { if (!doc) return try { await doc.cleanup() } catch { /* already released */ } } async function releaseLoadingTask(task: PDFDocumentLoadingTask | null) { if (!task || task.destroyed) return try { await task.destroy() } catch { /* already released */ } } type PdfPreviewViewerProps = { blobUrl: string name: string } function PdfPageCanvas({ pdf, pageNumber, containerWidth, zoom, shouldRender, }: { pdf: PDFDocumentProxy pageNumber: number containerWidth: number zoom: number shouldRender: boolean }) { const canvasRef = useRef(null) const renderTaskRef = useRef<{ cancel: () => void } | null>(null) useEffect(() => { if (!shouldRender || containerWidth <= 0 || zoom <= 0) return let cancelled = false const canvas = canvasRef.current if (!canvas) return ;(async () => { try { const page = await pdf.getPage(pageNumber) if (cancelled) return const base = page.getViewport({ scale: 1 }) const scale = fitScaleForPage(base.width, containerWidth) * zoom const viewport = page.getViewport({ scale }) const ctx = canvas.getContext("2d") if (!ctx) return const outputScale = window.devicePixelRatio || 1 canvas.width = Math.floor(viewport.width * outputScale) canvas.height = Math.floor(viewport.height * outputScale) canvas.style.width = `${viewport.width}px` canvas.style.height = `${viewport.height}px` ctx.setTransform(outputScale, 0, 0, outputScale, 0, 0) renderTaskRef.current?.cancel() const task = page.render({ canvasContext: ctx, viewport, canvas }) renderTaskRef.current = task await task.promise } catch { /* render cancelled or viewer unmounted */ } })() return () => { cancelled = true renderTaskRef.current?.cancel() renderTaskRef.current = null } }, [pdf, pageNumber, containerWidth, zoom, shouldRender]) return ( ) } function printPdfBlob(blobUrl: string, title: string) { const frame = document.createElement("iframe") frame.style.position = "fixed" frame.style.right = "0" frame.style.bottom = "0" frame.style.width = "0" frame.style.height = "0" frame.style.border = "0" frame.setAttribute("title", title) frame.src = blobUrl const cleanup = () => { frame.remove() } frame.onload = () => { try { frame.contentWindow?.focus() frame.contentWindow?.print() } finally { window.setTimeout(cleanup, 1000) } } document.body.appendChild(frame) } export function PdfPreviewViewer({ blobUrl, name }: PdfPreviewViewerProps) { const scrollRef = useRef(null) const pageRefs = useRef>(new Map()) const loadingTaskRef = useRef(null) const [pdf, setPdf] = useState(null) const [numPages, setNumPages] = useState(0) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [currentPage, setCurrentPage] = useState(1) const [containerWidth, setContainerWidth] = useState(0) const [zoom, setZoom] = useState(1) const [visiblePages, setVisiblePages] = useState>(() => new Set([1])) useEffect(() => { let destroyed = false let doc: PDFDocumentProxy | null = null loadingTaskRef.current = null setLoading(true) setError(null) setPdf(null) setNumPages(0) setCurrentPage(1) setZoom(1) setVisiblePages(new Set([1])) pageRefs.current.clear() ;(async () => { try { const response = await fetch(blobUrl) if (!response.ok) throw new Error("fetch failed") const data = await response.arrayBuffer() if (destroyed) return const task = getDocument({ data, isEvalSupported: false }) loadingTaskRef.current = task const loaded = await task.promise if (destroyed) { await releasePdfDocument(loaded) return } doc = loaded setPdf(loaded) setNumPages(loaded.numPages) setLoading(false) } catch { if (!destroyed) { setError("Impossible d’afficher ce PDF.") setLoading(false) } } })() return () => { destroyed = true void releaseLoadingTask(loadingTaskRef.current) void releasePdfDocument(doc) } }, [blobUrl]) const updateContainerWidth = useCallback(() => { const el = scrollRef.current if (!el) return const width = el.clientWidth - HORIZONTAL_PADDING_PX if (width > 0) setContainerWidth(width) }, []) useEffect(() => { if (!pdf) return updateContainerWidth() const el = scrollRef.current if (!el) return const ro = new ResizeObserver(updateContainerWidth) ro.observe(el) return () => ro.disconnect() }, [pdf, updateContainerWidth]) const updateCurrentPageFromScroll = useCallback(() => { const root = scrollRef.current if (!root || pageRefs.current.size === 0) return const focusY = root.getBoundingClientRect().top + root.clientHeight * 0.35 let bestPage = 1 let bestDist = Number.POSITIVE_INFINITY pageRefs.current.forEach((node, page) => { const dist = Math.abs(node.getBoundingClientRect().top - focusY) if (dist < bestDist) { bestDist = dist bestPage = page } }) setCurrentPage(bestPage) }, []) useEffect(() => { if (!pdf || numPages === 0) return const root = scrollRef.current if (!root) return const observer = new IntersectionObserver( (entries) => { setVisiblePages((prev) => { const merged = new Set(prev) for (const entry of entries) { const page = Number((entry.target as HTMLElement).dataset.page) if (!page) continue if (entry.isIntersecting) merged.add(page) } return merged }) updateCurrentPageFromScroll() }, { root, rootMargin: `${RENDER_MARGIN_PX}px 0px`, threshold: 0 }, ) const nodes = [...pageRefs.current.values()] nodes.forEach((node) => observer.observe(node)) root.addEventListener("scroll", updateCurrentPageFromScroll, { passive: true }) updateCurrentPageFromScroll() return () => { observer.disconnect() root.removeEventListener("scroll", updateCurrentPageFromScroll) } }, [pdf, numPages, updateCurrentPageFromScroll]) const scrollToPage = (page: number) => { const node = pageRefs.current.get(page) node?.scrollIntoView({ behavior: "smooth", block: "start" }) setCurrentPage(page) } const setPageRef = (page: number) => (node: HTMLDivElement | null) => { if (node) pageRefs.current.set(page, node) else pageRefs.current.delete(page) } if (loading) { return (
Ouverture du PDF…
) } if (error || !pdf) { return

{error ?? "Aperçu indisponible."}

} const zoomPercent = Math.round(zoom * 100) return (
{Array.from({ length: numPages }, (_, i) => { const pageNumber = i + 1 return (
Page {pageNumber}
0} />
) })}
{zoomPercent}%

Page {currentPage} / {numPages}

) }