'use client'; import { useState, useRef, useCallback } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import { SigningProgressBar } from './SigningProgressBar'; import type { SignatureFieldData } from '@/lib/db/schema'; // Worker setup — reuse same pattern as PdfViewerWrapper (no CDN, works in local/Docker) pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); // PDF rendered at a fixed width for consistent coordinate math const PDF_RENDER_WIDTH = 800; interface PageDimensions { renderedWidth: number; renderedHeight: number; originalWidth: number; originalHeight: number; } export interface SignatureCapture { fieldId: string; dataURL: string; } interface SigningPageClientProps { token: string; documentName: string; signatureFields: SignatureFieldData[]; /** Called when user clicks an unsigned field. Modal added in Plan 04. */ onFieldClick?: (fieldId: string) => void; } export function SigningPageClient({ token, documentName, signatureFields, onFieldClick, }: SigningPageClientProps) { const [numPages, setNumPages] = useState(0); // Map from page number (1-indexed) to rendered dimensions const [pageDimensions, setPageDimensions] = useState>({}); // Set of signed field IDs const [signedFields, setSignedFields] = useState>(new Set()); const [submitting, setSubmitting] = useState(false); // Exposed ref for Plan 04 to populate with captured signatures const signaturesRef = useRef([]); // Group fields by page for efficient lookup const fieldsByPage = signatureFields.reduce>( (acc, field) => { const page = field.page; if (!acc[page]) acc[page] = []; acc[page].push(field); return acc; }, {} ); const handlePageLoadSuccess = useCallback( (pageNum: number, page: { width: number; height: number; view: number[] }) => { setPageDimensions((prev) => ({ ...prev, [pageNum]: { renderedWidth: page.width, renderedHeight: page.height, originalWidth: Math.max(page.view[0], page.view[2]), originalHeight: Math.max(page.view[1], page.view[3]), }, })); }, [] ); const handleFieldClick = useCallback( (fieldId: string) => { if (signedFields.has(fieldId)) return; if (onFieldClick) { onFieldClick(fieldId); } }, [signedFields, onFieldClick] ); const handleJumpToNext = useCallback(() => { const nextUnsigned = signatureFields.find((f) => !signedFields.has(f.id)); if (nextUnsigned) { document.getElementById(`field-${nextUnsigned.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'center', }); } }, [signatureFields, signedFields]); const handleSubmit = useCallback(() => { if (signedFields.size < signatureFields.length || submitting) return; setSubmitting(true); // Submission POST handler added in Plan 04 }, [signedFields.size, signatureFields.length, submitting]); /** * Convert PDF user-space coordinates (bottom-left origin) to screen overlay position. * * PDF coords: origin at bottom-left, Y increases upward. * Screen coords: origin at top-left, Y increases downward. * * Formula: * screenTop = renderedHeight - (field.y / originalHeight * renderedHeight) - (field.height / originalHeight * renderedHeight) * screenLeft = field.x / originalWidth * renderedWidth */ const getFieldOverlayStyle = ( field: SignatureFieldData, dims: PageDimensions ): React.CSSProperties => { const scaleX = dims.renderedWidth / dims.originalWidth; const scaleY = dims.renderedHeight / dims.originalHeight; const top = dims.renderedHeight - field.y * scaleY - field.height * scaleY; const left = field.x * scaleX; const width = field.width * scaleX; const height = field.height * scaleY; return { position: 'absolute', top: `${top}px`, left: `${left}px`, width: `${width}px`, height: `${height}px`, cursor: 'pointer', borderRadius: '3px', animation: 'pulse-border 2s infinite', }; }; return ( <> {/* Pulse-border keyframes injected inline */}
{/* Branded header */}
Teressa Copeland Homes
{documentName}

Please review and sign the document below.

{/* PDF viewer */}
setNumPages(n)} loading={
Loading document...
} error={
Failed to load document. Please try refreshing.
} > {Array.from({ length: numPages }, (_, i) => { const pageNum = i + 1; const dims = pageDimensions[pageNum]; const fieldsOnPage = fieldsByPage[pageNum] ?? []; return (
handlePageLoadSuccess(pageNum, page)} renderAnnotationLayer={false} renderTextLayer={false} /> {/* Signature field overlays — rendered once page dimensions are known */} {dims && fieldsOnPage.map((field) => { const isSigned = signedFields.has(field.id); const overlayStyle = getFieldOverlayStyle(field, dims); return (
handleFieldClick(field.id)} role="button" tabIndex={0} aria-label={`Signature field${isSigned ? ' (signed)' : ' — click to sign'}`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick(field.id); } }} /> ); })}
); })}
{/* Sticky progress bar */} ); } // Export signed fields setter so Plan 04 can mark fields as signed after modal capture export type { SigningPageClientProps };