diff --git a/demo/server/server.js b/demo/server/server.js index 36d5b3c..f639d64 100644 --- a/demo/server/server.js +++ b/demo/server/server.js @@ -29,7 +29,7 @@ app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); -const normalizeFields = (fieldsPayload = {}) => { +const normalizeFields = (fieldsPayload = {}, signatureMode = 'annotate') => { const documentFields = Array.isArray(fieldsPayload.document) ? fieldsPayload.document : []; const signerFields = Array.isArray(fieldsPayload.signer) ? fieldsPayload.signer : []; @@ -38,8 +38,8 @@ const normalizeFields = (fieldsPayload = {}) => { .map((field) => { const isSignatureField = field.id === SIGNATURE_FIELD_ID; const value = field.value ?? ''; - const isDrawnSignature = typeof value === 'string' && value.startsWith('data:image/'); - const type = isSignatureField && isDrawnSignature ? 'signature' : 'text'; + const signatureType = signatureMode === 'sign' ? 'signature' : 'image'; + const type = isSignatureField ? signatureType : 'text'; const normalized = { id: field.id, value, type }; if (type === 'signature') { @@ -85,7 +85,8 @@ const sendPdfBuffer = (res, base64, fileName, contentType = 'application/pdf') = app.post('/v1/download', async (req, res) => { try { - const { document, fields = {}, fileName = 'document.pdf' } = req.body || {}; + const { document, fields = {}, fileName = 'document.pdf', signatureMode = 'annotate' } = + req.body || {}; if (!SUPERDOC_SERVICES_API_KEY) { return res.status(500).json({ error: 'Missing SUPERDOC_SERVICES_API_KEY on the server' }); @@ -95,7 +96,7 @@ app.post('/v1/download', async (req, res) => { return res.status(400).json({ error: 'document.url is required' }); } - const annotatedFields = normalizeFields(fields); + const annotatedFields = normalizeFields(fields, signatureMode); const { base64, contentType } = await annotateDocument({ documentUrl: document.url, @@ -129,6 +130,7 @@ app.post('/v1/sign', async (req, res) => { certificate, metadata, fileName = 'signed-document.pdf', + signatureMode = 'sign', } = req.body || {}; if (!SUPERDOC_SERVICES_API_KEY) { @@ -139,10 +141,13 @@ app.post('/v1/sign', async (req, res) => { return res.status(400).json({ error: 'document.url is required' }); } - const annotatedFields = normalizeFields({ - document: documentFields, - signer: signerFields, - }); + const annotatedFields = normalizeFields( + { + document: documentFields, + signer: signerFields, + }, + signatureMode, + ); const { base64: annotatedBase64 } = await annotateDocument({ documentUrl: document.url, diff --git a/demo/src/App.css b/demo/src/App.css index e7f97ae..6f47d6c 100644 --- a/demo/src/App.css +++ b/demo/src/App.css @@ -484,4 +484,60 @@ header { .superdoc-esign-actions { gap: 12px; +} + +/* Main layout container */ +.main-layout-container { + display: flex; + gap: 24px; +} + +/* Main content area */ +.main-content-area { + flex: 1; + min-width: 0; +} + +/* Right sidebar */ +.document-fields-sidebar { + width: 280px; + flex-shrink: 0; + padding: 16px; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + align-self: flex-start; +} + +.document-fields-sidebar h3 { + margin: 0 0 16px; + font-size: 14px; + font-weight: 600; + color: #374151; +} + +.document-fields-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.document-field { + /* Individual field styles handled inline for now */ +} + +/* Responsive layout - stack vertically on small screens */ +@media (max-width: 768px) { + .main-layout-container { + flex-direction: column; + } + + .document-fields-sidebar { + width: 100%; + order: 2; /* Move sidebar below the main content */ + } + + .main-content-area { + order: 1; + } } \ No newline at end of file diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 4940b1f..b78c8e7 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef } from 'react'; -import SuperDocESign from '@superdoc-dev/esign'; +import SuperDocESign, { textToImageDataUrl } from '@superdoc-dev/esign'; import type { SubmitData, SigningState, @@ -16,6 +16,54 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const documentSource = 'https://storage.googleapis.com/public_static_hosting/public_demo_docs/service_agreement_updated.docx'; +const signerFieldsConfig = [ + { + id: '789012', + type: 'signature' as const, + label: 'Your Signature', + validation: { required: true }, + component: CustomSignature, + }, + { + id: 'terms', + type: 'checkbox' as const, + label: 'I accept the terms and conditions', + validation: { required: true }, + }, + { + id: 'email', + type: 'checkbox' as const, + label: 'Send me a copy of the agreement', + validation: { required: false }, + }, +]; + +const signatureFieldIds = new Set( + signerFieldsConfig.filter((field) => field.type === 'signature').map((field) => field.id), +); + +const toSignatureImageValue = (value: SubmitData['signerFields'][number]['value']) => { + if (value === null || value === undefined) return null; + if (typeof value === 'string' && value.startsWith('data:image/')) return value; + return textToImageDataUrl(String(value)); +}; + +const mapSignerFieldsWithType = ( + fields: Array<{ id: string; value: SubmitData['signerFields'][number]['value'] }>, + signatureType: 'signature' | 'image', +) => + fields.map((field) => { + if (!signatureFieldIds.has(field.id)) { + return field; + } + + return { + ...field, + type: signatureType, + value: toSignatureImageValue(field.value), + }; + }); + // Helper to download a response blob as a file const downloadBlob = async (response: Response, fileName: string) => { const blob = await response.blob(); @@ -93,13 +141,15 @@ export function App() { console.log('Submit data:', data); try { + const signerFields = mapSignerFieldsWithType(data.signerFields, 'signature'); + const response = await fetch(`${API_BASE_URL}/v1/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ document: { url: documentSource }, documentFields: data.documentFields, - signerFields: data.signerFields, + signerFields, auditTrail: data.auditTrail, eventId: data.eventId, certificate: { enable: true }, @@ -108,6 +158,7 @@ export function App() { plan: documentFields['456789'], }, fileName: `signed_agreement_${data.eventId}.pdf`, + signatureMode: 'sign', }), }); @@ -134,13 +185,19 @@ export function App() { return; } + const signerFields = mapSignerFieldsWithType(data.fields.signer, 'image'); + const response = await fetch(`${API_BASE_URL}/v1/download`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ document: { url: data.documentSource }, - fields: data.fields, + fields: { + ...data.fields, + signer: signerFields, + }, fileName: data.fileName, + signatureMode: 'annotate', }), }); @@ -251,9 +308,9 @@ export function App() { Use the document toolbar to download the current agreement at any time.

-
+
{/* Main content */} -
+
{/* Right Sidebar - Document Fields */} -
-

- Document Fields -

-
+
+

Document Fields

+
{documentFieldsConfig.map((field) => (