import { FormSectionCard, FormSectionGrid } from "@repo/rdx-ui/components"; import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Spinner, } from "@repo/shadcn-ui/components"; import { DownloadIcon, ExternalLinkIcon, EyeIcon, PrinterIcon, ZoomInIcon, ZoomOutIcon, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; const DEFAULT_ZOOM_OPTIONS = [50, 75, 100, 125, 150]; const FIT_ZOOM_VALUE = "fit"; const PDF_PAGE_WIDTH = 794; const PDF_PAGE_HEIGHT = 1123; type PDFViewerProps = { src?: string | null; title?: string; description?: string; className?: string; loading?: boolean; error?: string | null; defaultZoom?: number | "fit"; zoomOptions?: number[]; onOpen?: () => void; onDownload?: () => void; onPrint?: () => void; }; const clampZoom = (value: number, zoomOptions: number[]) => { const min = Math.min(...zoomOptions); const max = Math.max(...zoomOptions); return Math.min(Math.max(value, min), max); }; export function PDFViewer({ src, title = "Vista previa", description = "Documento PDF embebido en la pantalla.", className, loading = false, error = null, defaultZoom = FIT_ZOOM_VALUE, zoomOptions = DEFAULT_ZOOM_OPTIONS, onOpen, onDownload, onPrint, }: PDFViewerProps) { const viewportRef = useRef(null); const [fitZoom, setFitZoom] = useState(100); const [zoom, setZoom] = useState(() => defaultZoom === FIT_ZOOM_VALUE ? FIT_ZOOM_VALUE : String(clampZoom(defaultZoom, zoomOptions)) ); const resolvedSrc = useMemo(() => { if (!src) { return null; } const separator = src.includes("#") ? "&" : "#"; return `${src}${separator}toolbar=0&navpanes=0&scrollbar=1&view=FitH`; }, [src]); useEffect(() => { if (zoom !== FIT_ZOOM_VALUE) { return; } const element = viewportRef.current; if (!element) { return; } const updateFitZoom = () => { const styles = window.getComputedStyle(element); const paddingX = Number.parseFloat(styles.paddingLeft) + Number.parseFloat(styles.paddingRight); const paddingY = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom); const availableWidth = Math.max(element.clientWidth - paddingX, 320); const availableHeight = Math.max(element.clientHeight - paddingY, 320); const widthScale = availableWidth / PDF_PAGE_WIDTH; const heightScale = availableHeight / PDF_PAGE_HEIGHT; const nextScale = Math.min(widthScale, heightScale, 1); setFitZoom(Math.max(Math.round(nextScale * 100), 25)); }; updateFitZoom(); const observer = new ResizeObserver(() => updateFitZoom()); observer.observe(element); window.addEventListener("resize", updateFitZoom); return () => { observer.disconnect(); window.removeEventListener("resize", updateFitZoom); }; }, [zoom]); const effectiveZoom = zoom === FIT_ZOOM_VALUE ? fitZoom : Number(zoom); const zoomScale = effectiveZoom / 100; const minZoom = Math.min(...zoomOptions); const maxZoom = Math.max(...zoomOptions); const handleOpen = () => { if (onOpen) { onOpen(); return; } if (src) { window.open(src, "_blank", "noopener,noreferrer"); } }; const handleDownload = () => { if (onDownload) { onDownload(); return; } if (!src) { return; } const link = document.createElement("a"); link.href = src; link.download = ""; link.click(); }; const handlePrint = () => { if (onPrint) { onPrint(); return; } if (src) { window.open(src, "_blank", "noopener,noreferrer"); } }; return ( } className="shadow-sm" contentClassName="space-y-5" description={description} icon={} title={title} >
{loading ? (
Cargando vista previa del PDF...
) : null} {!loading && error ? (

No se pudo cargar la vista previa

{error}

) : null} {!(loading || error) && resolvedSrc ? (