Skip to content

Instantly share code, notes, and snippets.

@dexit
Created September 22, 2025 11:42
Show Gist options
  • Save dexit/d16b71856ad6e16bd1378386a7907e24 to your computer and use it in GitHub Desktop.
Save dexit/d16b71856ad6e16bd1378386a7907e24 to your computer and use it in GitHub Desktop.
another compoennt
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