diff --git a/teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx b/teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx index 5f49192..6ef886b 100644 --- a/teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx +++ b/teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx @@ -10,9 +10,12 @@ interface SignatureModalProps { fieldId: string; onConfirm: (fieldId: string, dataURL: string, save: boolean) => void; onClose: () => void; + title?: string; // defaults to "Add Signature" } -export function SignatureModal({ isOpen, fieldId, onConfirm, onClose }: SignatureModalProps) { +export function SignatureModal({ isOpen, fieldId, onConfirm, onClose, title = 'Add Signature' }: SignatureModalProps) { + // Derive button label from title — "Add Initials" → "Apply Initials", otherwise "Apply Signature" + const buttonLabel = title.startsWith('Add ') ? title.replace('Add ', 'Apply ') : 'Apply Signature'; const [tab, setTab] = useState<'draw' | 'type' | 'saved'>('draw'); const [typedName, setTypedName] = useState(''); const [saveForLater, setSaveForLater] = useState(false); @@ -112,7 +115,7 @@ export function SignatureModal({ isOpen, fieldId, onConfirm, onClose }: Signatur > {/* Header */}
-

Add Signature

+

{title}

diff --git a/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx b/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx index aebcd53..99018bf 100644 --- a/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx +++ b/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx @@ -53,6 +53,7 @@ export function SigningPageClient({ // Modal state const [modalOpen, setModalOpen] = useState(false); const [activeFieldId, setActiveFieldId] = useState(null); + const [activeFieldType, setActiveFieldType] = useState<'client-signature' | 'initials'>('client-signature'); // Ref for all captured signatures (fieldId -> dataURL) const signaturesRef = useRef([]); @@ -87,11 +88,12 @@ export function SigningPageClient({ (fieldId: string) => { const field = signatureFields.find((f) => f.id === fieldId); if (!field) return; - // Defense-in-depth: primary protection is the server filter in GET /api/sign/[token] - // Only client-signature fields open the modal; Phase 10 will expand this for initials - if (getFieldType(field) !== 'client-signature') return; + const ft = getFieldType(field); + // Only client-signature and initials require client action + if (ft !== 'client-signature' && ft !== 'initials') return; if (signedFields.has(fieldId)) return; setActiveFieldId(fieldId); + setActiveFieldType(ft as 'client-signature' | 'initials'); setModalOpen(true); }, [signatureFields, signedFields] @@ -122,7 +124,9 @@ export function SigningPageClient({ ); const handleJumpToNext = useCallback(() => { - const nextUnsigned = signatureFields.find((f) => !signedFields.has(f.id)); + const nextUnsigned = signatureFields.find( + (f) => (getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials') && !signedFields.has(f.id) + ); if (nextUnsigned) { document.getElementById(`field-${nextUnsigned.id}`)?.scrollIntoView({ behavior: 'smooth', @@ -132,10 +136,10 @@ export function SigningPageClient({ }, [signatureFields, signedFields]); const handleSubmit = useCallback(async () => { - const clientSigFields = signatureFields.filter( - (f) => getFieldType(f) === 'client-signature' + const requiredFields = signatureFields.filter( + (f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials' ); - if (signedFields.size < clientSigFields.length || submitting) return; + if (signedFields.size < requiredFields.length || submitting) return; setSubmitting(true); try { const res = await fetch(`/api/sign/${token}`, { @@ -205,6 +209,10 @@ export function SigningPageClient({ box-shadow: 0 0 0 3px #3b82f6, 0 0 16px 4px rgba(59,130,246,0.6); } } + @keyframes pulse-border-purple { + 0%, 100% { box-shadow: 0 0 0 2px #7c3aed, 0 0 8px 2px rgba(124,58,237,0.4); } + 50% { box-shadow: 0 0 0 3px #7c3aed, 0 0 16px 4px rgba(124,58,237,0.6); } + } .signing-field-signed { box-shadow: 0 0 0 2px #22c55e !important; animation: none !important; @@ -288,19 +296,31 @@ export function SigningPageClient({ {/* Signature field overlays — rendered once page dimensions are known */} {dims && fieldsOnPage.map((field) => { + // Only render interactive overlays for client-signature and initials fields + // text/checkbox/date are embedded at prepare time — no client interaction needed + const ft = getFieldType(field); + const isInteractive = ft === 'client-signature' || ft === 'initials'; + if (!isInteractive) return null; + const isSigned = signedFields.has(field.id); - const overlayStyle = getFieldOverlayStyle(field, dims); + const baseStyle = getFieldOverlayStyle(field, dims); + const animationStyle: React.CSSProperties = ft === 'initials' + ? { animation: 'pulse-border-purple 2s infinite' } + : { animation: 'pulse-border 2s infinite' }; + const fieldOverlayStyle = { ...baseStyle, ...animationStyle }; const sigDataURL = signedFields.get(field.id); return (
handleFieldClick(field.id)} role="button" tabIndex={0} - aria-label={`Signature field${isSigned ? ' (signed)' : ' — click to sign'}`} + aria-label={ft === 'initials' + ? `Initials field${isSigned ? ' (initialed)' : ' — click to initial'}` + : `Signature field${isSigned ? ' (signed)' : ' — click to sign'}`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -308,12 +328,12 @@ export function SigningPageClient({ } }} > - {/* Show signature preview when signed */} + {/* Show signature/initials preview when signed */} {isSigned && sigDataURL && ( /* eslint-disable-next-line @next/next/no-img-element */ Signature getFieldType(f) === 'client-signature').length} + total={signatureFields.filter( + (f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials' + ).length} signed={signedFields.size} onJumpToNext={handleJumpToNext} onSubmit={handleSubmit} @@ -345,6 +367,7 @@ export function SigningPageClient({ { setModalOpen(false);