Initial LinkedSign platform

This commit is contained in:
2026-04-06 20:33:59 -05:00
commit 1074c7c9d2
159 changed files with 19072 additions and 0 deletions
@@ -0,0 +1,221 @@
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';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
const SIG_W = 200;
const SIG_H = 60;
const getSignatureWidth = (style, signerName = '') => {
if (style !== 'qr') return SIG_W;
const normalizedName = signerName.trim();
const calculatedWidth = 78 + (normalizedName.length * 4.9);
return Math.max(170, Math.min(270, Math.round(calculatedWidth)));
};
export default function PdfSignaturePlacer({
pdfBlob,
signerName,
signatureStyle = 'classic',
qrPosition = 'left',
qrBackground = 'transparent',
qrBorder = 'none',
signatureImage = null,
onPlaced,
}) {
const [numPages, setNumPages] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [sigBox, setSigBox] = useState(null); // { x, y } in container px
const containerRef = useRef(null);
const [pageSize, setPageSize] = useState(null); // rendered px size
const signatureWidth = getSignatureWidth(signatureStyle, signerName);
const getCanvasMetrics = () => {
const container = containerRef.current;
const canvas = container?.querySelector('.react-pdf__Page__canvas');
if (!container || !canvas) return null;
const containerRect = container.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
return {
containerRect,
canvasRect,
offsetX: canvasRect.left - containerRect.left,
offsetY: canvasRect.top - containerRect.top,
width: canvasRect.width,
height: canvasRect.height,
};
};
const handleDocLoad = ({ numPages }) => setNumPages(numPages);
const handlePageLoad = (page) => {
setPageSize({ width: page.width, height: page.height });
};
const handleContainerClick = (e) => {
const metrics = getCanvasMetrics();
if (!metrics) return;
const x = e.clientX - metrics.canvasRect.left;
const y = e.clientY - metrics.canvasRect.top;
const clampedX = Math.min(Math.max(x, 0), metrics.width - signatureWidth);
const clampedY = Math.min(Math.max(y, 0), metrics.height - SIG_H);
setSigBox({
x: metrics.offsetX + clampedX,
y: metrics.offsetY + clampedY,
});
};
const handleConfirm = () => {
if (!sigBox || !pageSize) return;
const metrics = getCanvasMetrics();
if (!metrics) return;
const relativeX = sigBox.x - metrics.offsetX;
const relativeY = sigBox.y - metrics.offsetY;
// PDF coordinates: origin bottom-left, Y inverted
const scaleX = pageSize.pdfWidth / metrics.width;
const scaleY = pageSize.pdfHeight / metrics.height;
const pdfX = relativeX * scaleX;
const pdfY = pageSize.pdfHeight - (relativeY + SIG_H) * scaleY;
onPlaced({
page: currentPage - 1,
rect: {
x: Math.round(pdfX),
y: Math.round(pdfY),
width: Math.round(signatureWidth * scaleX),
height: Math.round(SIG_H * scaleY),
},
});
};
const previewDate = new Date().toLocaleDateString('es-EC');
const renderPreview = () => {
if (signatureStyle === 'qr') {
return (
<div className="sig-preview-body sig-preview-body--qr">
{qrPosition === 'left' && (
<div className="sig-preview-qr-box" data-background={qrBackground} data-border={qrBorder}>
<div className="sig-preview-qr-pattern" />
<span>QR</span>
</div>
)}
<div className="sig-preview-text">
<strong>{signerName}</strong>
<small>Verificación digital</small>
</div>
{qrPosition === 'right' && (
<div className="sig-preview-qr-box" data-background={qrBackground} data-border={qrBorder}>
<div className="sig-preview-qr-pattern" />
<span>QR</span>
</div>
)}
</div>
);
}
if (signatureStyle === 'professional') {
return (
<div className="sig-preview-body sig-preview-body--professional">
<div className="sig-preview-pro-header">FIRMA DIGITAL</div>
<div className="sig-preview-pro-content">
<strong>{signerName}</strong>
<small>{previewDate}</small>
</div>
</div>
);
}
if (signatureStyle === 'image') {
return (
<div className="sig-preview-body sig-preview-body--image">
<div className="sig-preview-image-area">
{signatureImage
? <img src={signatureImage} alt="firma" className="sig-preview-image" />
: <span className="sig-preview-image-placeholder">Firma</span>}
</div>
<div className="sig-preview-text">
<strong>{signerName}</strong>
<small>Firma digital · {previewDate}</small>
</div>
</div>
);
}
return (
<div className="sig-preview-body">
<span className="sig-preview-badge">DS</span>
<div className="sig-preview-text">
<strong>{signerName}</strong>
<small>Firma digital · {previewDate}</small>
</div>
</div>
);
};
return (
<div className="pdf-placer">
<div className="pdf-placer-toolbar">
<span className="pdf-placer-hint">
Haz clic en el documento para colocar la firma
</span>
<div className="pdf-placer-pages">
<button className="btn btn-ghost" disabled={currentPage <= 1}
onClick={() => { setCurrentPage(p => p - 1); setSigBox(null); }}>‹</button>
<span>Pág. {currentPage} / {numPages || '…'}</span>
<button className="btn btn-ghost" disabled={currentPage >= numPages}
onClick={() => { setCurrentPage(p => p + 1); setSigBox(null); }}>›</button>
</div>
</div>
<div
ref={containerRef}
className="pdf-placer-canvas"
onClick={handleContainerClick}
style={{ cursor: 'crosshair', position: 'relative', display: 'inline-block' }}
>
<Document file={pdfBlob} onLoadSuccess={handleDocLoad}>
<Page
pageNumber={currentPage}
width={600}
onLoadSuccess={(p) => setPageSize({
width: p.width, height: p.height,
pdfWidth: p.originalWidth, pdfHeight: p.originalHeight,
})}
/>
</Document>
{sigBox && (
<div
className={`sig-preview-box sig-preview-box--${signatureStyle}`}
data-background={signatureStyle === 'qr' ? qrBackground : undefined}
data-border={signatureStyle === 'qr' ? qrBorder : undefined}
style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }}
onClick={(e) => e.stopPropagation()}
>
{signatureStyle === 'classic' && <div className="sig-preview-bar" />}
{renderPreview()}
</div>
)}
</div>
{sigBox && (
<div className="pdf-placer-actions">
<button className="btn btn-ghost" onClick={() => setSigBox(null)}>Limpiar</button>
<button className="btn btn-primary" onClick={handleConfirm}>
Confirmar posición
</button>
</div>
)}
</div>
);
}