Created
          September 22, 2025 11:42 
        
      - 
      
- 
        Save dexit/d16b71856ad6e16bd1378386a7907e24 to your computer and use it in GitHub Desktop. 
    another compoennt
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { useState, useRef, useEffect } from 'react'; | |
| type Recipient = { | |
| id: string; | |
| name: string; | |
| email: string; | |
| hasSigned: boolean; | |
| certificateId: string | null; | |
| }; | |
| type OverlayElement = { | |
| id: string; | |
| type: 'signature' | 'initials' | 'fullName' | 'date'; | |
| recipientId: string; | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number; | |
| }; | |
| type DocumentStatus = 'Draft' | 'Pending Signatures' | 'Signed' | 'Accepted' | 'Rejected'; | |
| const availableCertificates = [ | |
| { id: 'cert_1', name: 'John Doe Signature Cert', issuer: 'DigiSign Corp', validity: '2023-2025' }, | |
| { id: 'cert_2', name: 'Company A Document Signer', issuer: 'GlobalTrust CA', validity: '2024-2026' }, | |
| ]; | |
| const generateId = () => Math.random().toString(36).substring(2, 9); | |
| export default function DigitalSignatureApp() { | |
| const [step, setStep] = useState(0); | |
| const [documentName, setDocumentName] = useState(''); | |
| const [base64Pdf, setBase64Pdf] = useState<string | null>(null); | |
| const [requester, setRequester] = useState({ name: '', email: '' }); | |
| const [recipients, setRecipients] = useState<Recipient[]>([]); | |
| const [overlayElements, setOverlayElements] = useState<OverlayElement[]>([]); | |
| const [documentStatus, setDocumentStatus] = useState<DocumentStatus>('Draft'); | |
| const [signingError, setSigningError] = useState<string | null>(null); | |
| const [pinInput, setPinInput] = useState(''); | |
| const [showSigningModal, setShowSigningModal] = useState(false); | |
| const [activeRecipient, setActiveRecipient] = useState<Recipient | null>(null); | |
| const [selectedRecipient, setSelectedRecipient] = useState<string | null>(null); | |
| const pdfViewerRef = useRef<HTMLDivElement>(null); | |
| const [isPlacing, setIsPlacing] = useState(false); | |
| const [draggingType, setDraggingType] = useState<OverlayElement['type'] | null>(null); | |
| const handleDocumentUpload = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| if (e.target.files && e.target.files[0]) { | |
| const file = e.target.files[0]; | |
| setDocumentName(file.name); | |
| const reader = new FileReader(); | |
| reader.onloadend = () => setBase64Pdf(reader.result as string); | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| const handleAddRecipient = () => { | |
| setRecipients([...recipients, { | |
| id: generateId(), | |
| name: '', | |
| email: '', | |
| hasSigned: false, | |
| certificateId: null | |
| }]); | |
| }; | |
| const handleRemoveRecipient = (id: string) => { | |
| setRecipients(recipients.filter(r => r.id !== id)); | |
| setOverlayElements(overlayElements.filter(o => o.recipientId !== id)); | |
| }; | |
| const handleRecipientChange = (id: string, field: keyof Recipient, value: string) => { | |
| setRecipients(recipients.map(r => r.id === id ? { ...r, [field]: value } : r)); | |
| }; | |
| const handleDragStart = (type: OverlayElement['type']) => { | |
| if (!selectedRecipient) { | |
| setSigningError('Please select a recipient first.'); | |
| return; | |
| } | |
| setDraggingType(type); | |
| setIsPlacing(true); | |
| setSigningError(null); | |
| }; | |
| const handlePdfClick = (e: React.MouseEvent<HTMLDivElement>) => { | |
| if (!isPlacing || !draggingType || !pdfViewerRef.current || !selectedRecipient) return; | |
| const viewerRect = pdfViewerRef.current.getBoundingClientRect(); | |
| const x = ((e.clientX - viewerRect.left) / viewerRect.width) * 100; | |
| const y = ((e.clientY - viewerRect.top) / viewerRect.height) * 100; | |
| const width = 15; | |
| const height = draggingType === 'signature' ? 8 : 5; | |
| setOverlayElements(prev => [ | |
| ...prev, | |
| { | |
| id: generateId(), | |
| type: draggingType, | |
| recipientId: selectedRecipient, | |
| x: Math.max(0, Math.min(100 - width, x)), | |
| y: Math.max(0, Math.min(100 - height, y)), | |
| width, | |
| height, | |
| } | |
| ]); | |
| setIsPlacing(false); | |
| setDraggingType(null); | |
| }; | |
| const initiateSigning = (recipient: Recipient) => { | |
| setActiveRecipient(recipient); | |
| setShowSigningModal(true); | |
| setPinInput(''); | |
| setSigningError(null); | |
| }; | |
| const handleConfirmSigning = () => { | |
| if (!activeRecipient) { | |
| setSigningError("No active recipient."); | |
| return; | |
| } | |
| if (pinInput === '1234') { | |
| setRecipients(recipients.map(r => | |
| r.id === activeRecipient.id ? { ...r, hasSigned: true } : r | |
| )); | |
| setShowSigningModal(false); | |
| const allSigned = recipients.every(r => r.hasSigned || r.id === activeRecipient.id); | |
| if (allSigned) { | |
| setStep(5); | |
| } | |
| } else { | |
| setSigningError('Invalid PIN. Please try again.'); | |
| } | |
| }; | |
| const handleSaveDocument = (submit: boolean) => { | |
| const status: DocumentStatus = submit | |
| ? (recipients.every(r => r.hasSigned) ? 'Signed' : 'Pending Signatures') | |
| : 'Draft'; | |
| setDocumentStatus(status); | |
| alert(`Document ${submit ? 'submitted' : 'saved'} as ${status}.`); | |
| setStep(0); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-indigo-50 to-purple-100 p-6"> | |
| <div className="max-w-6xl mx-auto bg-white rounded-2xl shadow-xl p-6"> | |
| <h1 className="text-3xl font-bold text-center text-indigo-800 mb-6"> | |
| Digital Signature App | |
| </h1> | |
| {/* Step Indicator */} | |
| <div className="flex justify-between mb-8 relative"> | |
| <div className="absolute top-1/2 left-0 right-0 h-1 bg-gray-200 -z-10"> | |
| <div | |
| className="h-full bg-indigo-500 transition-all duration-500" | |
| style={{ width: `${(step / 5) * 100}%` }} | |
| ></div> | |
| </div> | |
| {['Start', 'Parties', 'Upload', 'Prepare', 'Sign', 'Review'].map((label, i) => ( | |
| <div key={i} className="flex flex-col items-center"> | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center ${ | |
| i <= step ? 'bg-indigo-600 text-white' : 'bg-gray-300 text-gray-600' | |
| }`}> | |
| {i + 1} | |
| </div> | |
| <span className="text-xs mt-1">{label}</span> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Step Content */} | |
| <div className="min-h-[500px]"> | |
| {/* Step 0: Start */} | |
| {step === 0 && ( | |
| <div className="flex flex-col items-center justify-center h-full py-12"> | |
| <h2 className="text-2xl font-bold mb-4">Welcome to Digital Signatures</h2> | |
| <p className="text-gray-600 mb-8 max-w-md text-center"> | |
| Securely sign documents with PKI-based digital signatures | |
| </p> | |
| <button | |
| onClick={() => setStep(1)} | |
| className="bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700" | |
| > | |
| Start New Document | |
| </button> | |
| </div> | |
| )} | |
| {/* Step 1: Define Parties */} | |
| {step === 1 && ( | |
| <div className="py-6"> | |
| <h2 className="text-2xl font-bold mb-6 text-center">Define Document Parties</h2> | |
| <div className="mb-8 p-6 bg-blue-50 rounded-xl"> | |
| <h3 className="text-xl font-semibold mb-4">Requester Information</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium mb-1">Name</label> | |
| <input | |
| type="text" | |
| value={requester.name} | |
| onChange={(e) => setRequester({...requester, name: e.target.value})} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-1">Email</label> | |
| <input | |
| type="email" | |
| value={requester.email} | |
| onChange={(e) => setRequester({...requester, email: e.target.value})} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-6 bg-purple-50 rounded-xl"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="text-xl font-semibold">Recipients</h3> | |
| <button | |
| onClick={handleAddRecipient} | |
| className="bg-purple-600 text-white px-4 py-2 rounded-lg text-sm" | |
| > | |
| Add Recipient | |
| </button> | |
| </div> | |
| {recipients.map((rec, i) => ( | |
| <div key={rec.id} className="mb-4 p-4 border rounded-lg relative"> | |
| <button | |
| onClick={() => handleRemoveRecipient(rec.id)} | |
| className="absolute top-2 right-2 text-red-500" | |
| > | |
| ✕ | |
| </button> | |
| <p className="font-semibold mb-3">Recipient {i + 1}</p> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium mb-1">Name</label> | |
| <input | |
| type="text" | |
| value={rec.name} | |
| onChange={(e) => handleRecipientChange(rec.id, 'name', e.target.value)} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-1">Email</label> | |
| <input | |
| type="email" | |
| value={rec.email} | |
| onChange={(e) => handleRecipientChange(rec.id, 'email', e.target.value)} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {signingError && ( | |
| <p className="text-red-600 bg-red-100 p-3 rounded-lg my-4"> | |
| {signingError} | |
| </p> | |
| )} | |
| <div className="flex justify-between mt-8"> | |
| <button | |
| onClick={() => setStep(0)} | |
| className="bg-gray-200 px-6 py-3 rounded-lg font-semibold" | |
| > | |
| Back | |
| </button> | |
| <button | |
| onClick={() => setStep(2)} | |
| disabled={!requester.name || !requester.email || recipients.length === 0 || recipients.some(r => !r.name || !r.email)} | |
| className={`px-6 py-3 rounded-lg font-semibold ${ | |
| requester.name && requester.email && recipients.length > 0 && !recipients.some(r => !r.name || !r.email) | |
| ? 'bg-indigo-600 text-white hover:bg-indigo-700' | |
| : 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Step 2: Upload Document */} | |
| {step === 2 && ( | |
| <div className="flex flex-col items-center justify-center py-12"> | |
| <h2 className="text-2xl font-bold mb-6">Upload Document</h2> | |
| <div className="w-full max-w-md bg-white border-2 border-dashed border-indigo-300 rounded-xl p-8 mb-6"> | |
| <svg className="w-16 h-16 text-indigo-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg> | |
| <p className="mb-2 text-center">Drag and drop your PDF here, or</p> | |
| <label className="cursor-pointer bg-indigo-500 text-white px-6 py-2 rounded-lg font-medium hover:bg-indigo-600 block text-center"> | |
| Browse Files | |
| <input | |
| type="file" | |
| className="hidden" | |
| onChange={handleDocumentUpload} | |
| accept=".pdf" | |
| /> | |
| </label> | |
| </div> | |
| {documentName && ( | |
| <p className="text-gray-700 mt-2">Selected: <span className="font-semibold">{documentName}</span></p> | |
| )} | |
| <div className="flex justify-between w-full max-w-md mt-8"> | |
| <button | |
| onClick={() => setStep(1)} | |
| className="bg-gray-200 px-6 py-3 rounded-lg font-semibold" | |
| > | |
| Back | |
| </button> | |
| <button | |
| onClick={() => setStep(3)} | |
| disabled={!base64Pdf} | |
| className={`px-6 py-3 rounded-lg font-semibold ${ | |
| base64Pdf | |
| ? 'bg-indigo-600 text-white hover:bg-indigo-700' | |
| : 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Step 3: Prepare Document */} | |
| {step === 3 && ( | |
| <div className="py-4"> | |
| <h2 className="text-2xl font-bold mb-4 text-center">Prepare Document</h2> | |
| <div className="flex flex-col lg:flex-row gap-6"> | |
| {/* Toolbar */} | |
| <div className="lg:w-1/4 p-4 bg-white border rounded-xl"> | |
| <h3 className="text-lg font-semibold mb-4">Place Signature Fields</h3> | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium mb-1">Select Recipient</label> | |
| <select | |
| value={selectedRecipient || ''} | |
| onChange={(e) => setSelectedRecipient(e.target.value)} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| > | |
| <option value="" disabled>-- Select Recipient --</option> | |
| {recipients.map(rec => ( | |
| <option key={rec.id} value={rec.id}>{rec.name}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="space-y-2"> | |
| {(['signature', 'initials', 'fullName', 'date'] as const).map(type => ( | |
| <button | |
| key={type} | |
| onClick={() => handleDragStart(type)} | |
| disabled={!selectedRecipient} | |
| className={`w-full py-2 rounded-lg font-medium ${ | |
| !selectedRecipient | |
| ? 'bg-gray-200 text-gray-500 cursor-not-allowed' | |
| : 'bg-indigo-500 text-white hover:bg-indigo-600' | |
| }`} | |
| > | |
| {type === 'signature' ? 'Signature' : type === 'initials' ? 'Initials' : type === 'fullName' ? 'Full Name' : 'Date'} | |
| </button> | |
| ))} | |
| </div> | |
| {isPlacing && ( | |
| <p className="text-sm text-indigo-700 mt-4 animate-pulse"> | |
| Click on the document to place the {draggingType} field. | |
| </p> | |
| )} | |
| </div> | |
| {/* PDF Viewer */} | |
| <div className="lg:w-3/4"> | |
| <div className="bg-white border rounded-xl p-4"> | |
| <h3 className="text-lg font-semibold mb-3">Document Preview</h3> | |
| <div | |
| ref={pdfViewerRef} | |
| className="relative w-full h-[500px] border rounded-lg overflow-auto bg-gray-100" | |
| onClick={handlePdfClick} | |
| > | |
| {base64Pdf ? ( | |
| <div className="absolute inset-0 flex items-center justify-center text-gray-500"> | |
| PDF Preview: {documentName} | |
| </div> | |
| ) : ( | |
| <div className="absolute inset-0 flex items-center justify-center text-gray-500"> | |
| Upload a PDF to view here | |
| </div> | |
| )} | |
| {overlayElements.map(el => { | |
| const recipient = recipients.find(r => r.id === el.recipientId); | |
| const displayValue = el.type === 'fullName' | |
| ? (recipient?.name || '[Name]') | |
| : el.type === 'date' | |
| ? new Date().toLocaleDateString() | |
| : `[${el.type}]`; | |
| return ( | |
| <div | |
| key={el.id} | |
| className="absolute border-2 border-dashed rounded p-1 bg-white text-xs font-semibold cursor-pointer" | |
| style={{ | |
| left: `${el.x}%`, | |
| top: `${el.y}%`, | |
| width: `${el.width}%`, | |
| height: `${el.height}%`, | |
| }} | |
| > | |
| {displayValue} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {signingError && ( | |
| <p className="text-red-600 bg-red-100 p-3 rounded-lg my-4"> | |
| {signingError} | |
| </p> | |
| )} | |
| <div className="flex justify-between mt-6"> | |
| <button | |
| onClick={() => setStep(2)} | |
| className="bg-gray-200 px-6 py-3 rounded-lg font-semibold" | |
| > | |
| Back | |
| </button> | |
| <button | |
| onClick={() => setStep(4)} | |
| disabled={overlayElements.length === 0} | |
| className={`px-6 py-3 rounded-lg font-semibold ${ | |
| overlayElements.length > 0 | |
| ? 'bg-indigo-600 text-white hover:bg-indigo-700' | |
| : 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| Proceed to Signatures | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Step 4: Signatures */} | |
| {step === 4 && ( | |
| <div className="py-8"> | |
| <h2 className="text-2xl font-bold mb-6 text-center">Digital Signatures</h2> | |
| <div className="max-w-2xl mx-auto"> | |
| {recipients.map(rec => ( | |
| <div key={rec.id} className="p-6 bg-white border rounded-xl mb-4"> | |
| <h3 className="text-xl font-semibold mb-3">Signee: {rec.name}</h3> | |
| {rec.hasSigned ? ( | |
| <div className="text-green-700 bg-green-100 border border-green-300 rounded-lg px-4 py-3"> | |
| ✓ Signed Successfully! | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium mb-1">Select Certificate</label> | |
| <select | |
| value={rec.certificateId || ''} | |
| onChange={(e) => handleRecipientChange(rec.id, 'certificateId', e.target.value)} | |
| className="w-full px-4 py-2 border rounded-lg" | |
| > | |
| <option value="" disabled>-- Select Certificate --</option> | |
| {availableCertificates.map(cert => ( | |
| <option key={cert.id} value={cert.id}> | |
| {cert.name} ({cert.issuer}) | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| <button | |
| onClick={() => initiateSigning(rec)} | |
| disabled={!rec.certificateId} | |
| className={`w-full px-6 py-3 rounded-lg font-semibold ${ | |
| rec.certificateId | |
| ? 'bg-indigo-600 text-white hover:bg-indigo-700' | |
| : 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| Sign as {rec.name} | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| <div className="flex justify-between mt-8"> | |
| <button | |
| onClick={() => setStep(3)} | |
| className="bg-gray-200 px-6 py-3 rounded-lg font-semibold" | |
| > | |
| Back | |
| </button> | |
| <button | |
| onClick={() => setStep(5)} | |
| disabled={!recipients.every(r => r.hasSigned)} | |
| className={`px-6 py-3 rounded-lg font-semibold ${ | |
| recipients.every(r => r.hasSigned) | |
| ? 'bg-indigo-600 text-white hover:bg-indigo-700' | |
| : 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| Finalize Document | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Step 5: Review */} | |
| {step === 5 && ( | |
| <div className="flex flex-col items-center justify-center py-12"> | |
| <div className="bg-green-100 rounded-full p-4 mb-6"> | |
| <svg className="w-16 h-16 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2l4-4m6 2a9 9 0 11-18 0a9 9 0 0118 0z" /> | |
| </svg> | |
| </div> | |
| <h2 className="text-3xl font-bold text-green-800 mb-4">Document Processed!</h2> | |
| <p className="text-gray-700 mb-8 max-w-md text-center"> | |
| Your document "{documentName}" has been successfully prepared and signed. | |
| </p> | |
| <div className="flex space-x-4"> | |
| <button | |
| onClick={() => handleSaveDocument(true)} | |
| className="bg-indigo-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-indigo-700" | |
| > | |
| Submit Document | |
| </button> | |
| <button | |
| onClick={() => handleSaveDocument(false)} | |
| className="bg-gray-200 text-gray-800 px-8 py-3 rounded-lg font-semibold hover:bg-gray-300" | |
| > | |
| Save as Draft | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Signing Modal */} | |
| {showSigningModal && activeRecipient && ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> | |
| <div className="bg-white rounded-xl p-8 max-w-md w-full"> | |
| <h3 className="text-2xl font-bold mb-4 text-center">Enter PIN</h3> | |
| <p className="text-gray-600 mb-6 text-center"> | |
| Enter PIN for {activeRecipient.name}'s certificate | |
| </p> | |
| <input | |
| type="password" | |
| value={pinInput} | |
| onChange={(e) => setPinInput(e.target.value)} | |
| placeholder="Enter your PIN" | |
| className="w-full px-4 py-3 border rounded-lg text-center text-xl mb-6" | |
| maxLength={6} | |
| /> | |
| {signingError && ( | |
| <p className="text-red-600 bg-red-100 p-3 rounded-lg mb-4"> | |
| {signingError} | |
| </p> | |
| )} | |
| <div className="flex justify-center space-x-4"> | |
| <button | |
| onClick={() => setShowSigningModal(false)} | |
| className="bg-gray-200 px-6 py-3 rounded-lg font-semibold" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleConfirmSigning} | |
| className="bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700" | |
| disabled={pinInput.length < 4} | |
| > | |
| Confirm Sign | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment