Skip to content

Instantly share code, notes, and snippets.

@dexit
Created September 22, 2025 11:41
Show Gist options
  • Select an option

  • Save dexit/0d4eae02efc2e194b6b92d1b93f629c3 to your computer and use it in GitHub Desktop.

Select an option

Save dexit/0d4eae02efc2e194b6b92d1b93f629c3 to your computer and use it in GitHub Desktop.
another component
"use client";
import { useState, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
Plus,
Download,
Upload,
Shield,
User,
Check,
X,
Mail,
Eye
} from "lucide-react";
// Simple Modal Component
const Modal = ({
isOpen,
onClose,
children
}: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="relative w-full max-w-md rounded-lg bg-white p-6 shadow-lg">
<button
onClick={onClose}
className="absolute right-4 top-4 rounded-full p-1 hover:bg-gray-100"
>
<X className="h-5 w-5" />
</button>
{children}
</div>
</div>
);
};
// Mock PDF.js worker implementation
class MockPDFWorker {
private isInitialized = false;
async init() {
if (this.isInitialized) return;
// Simulate worker initialization
console.log("Initializing PDF.js worker...");
await new Promise(resolve => setTimeout(resolve, 500));
this.isInitialized = true;
console.log("PDF.js worker initialized");
}
async loadDocument(file: File) {
if (!this.isInitialized) {
throw new Error("Worker not initialized");
}
// Simulate document loading
console.log("Loading PDF document...");
await new Promise(resolve => setTimeout(resolve, 300));
return {
numPages: 3,
getPage: async (pageNum: number) => ({
pageNumber: pageNum,
getContent: async () => ({ items: [] }),
getViewport: () => ({ width: 600, height: 800 })
})
};
}
async renderPage(page: any, canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Simulate rendering
console.log(`Rendering page ${page.pageNumber}`);
ctx.fillStyle = '#f3f4f6';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#374151';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText(`Page ${page.pageNumber}`, canvas.width/2, canvas.height/2);
ctx.font = '14px Arial';
ctx.fillText('PDF Preview', canvas.width/2, canvas.height/2 + 40);
}
async addSignature(pageNum: number, position: { x: number; y: number }) {
console.log(`Adding signature to page ${pageNum} at (${position.x}, ${position.y})`);
return `signature-${Date.now()}`;
}
async encryptDocument() {
console.log("Encrypting document...");
await new Promise(resolve => setTimeout(resolve, 800));
console.log("Document encrypted successfully");
}
async downloadDocument() {
console.log("Downloading document...");
// Simulate download
const link = document.createElement('a');
link.href = 'data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA0IDAgUgo+Pgo+PgovQ29udGVudHMgNSAwIFIKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0xlbmd0aCA0NDQKPj4Kc3RyZWFtCkJUCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIFdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgNQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE2IDAwMDAwIG4gCjAwMDAwMDAyNDAgMDAwMDAgbiAKMDAwMDAwMDM0NCAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDYKL1Jvb3QgMSAwIFIKL0luZm8gPDwKL0NyZWF0b3IgKFNjcmlwdGFzY3JpcHQpCi9Qcm9kdWNlciAoU2NyaXB0YXNjcmlwdCkKPj4KPj4Kc3RhcnR4cmVmCjQ2MgolJUVPRgo=';
link.download = 'signed-document.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
export default function PDFSignerAdvanced() {
const [pdfFile, setPdfFile] = useState<File | null>(null);
const [recipients, setRecipients] = useState([
{ id: "1", name: "John Doe", email: "[email protected]", signed: false },
{ id: "2", name: "Jane Smith", email: "[email protected]", signed: true },
]);
const [newRecipient, setNewRecipient] = useState({ name: "", email: "" });
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSigned, setIsSigned] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [signingPosition, setSigningPosition] = useState<{ x: number; y: number } | null>(null);
const [isSigningMode, setIsSigningMode] = useState(false);
const [signatures, setSignatures] = useState<Record<number, string>>({});
const fileInputRef = useRef<HTMLInputElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const workerRef = useRef<MockPDFWorker>(new MockPDFWorker());
const documentRef = useRef<any>(null);
useEffect(() => {
workerRef.current.init();
}, []);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && file.type === "application/pdf") {
setPdfFile(file);
setIsSigned(false);
setSignatures({});
try {
documentRef.current = await workerRef.current.loadDocument(file);
setTotalPages(documentRef.current.numPages);
setCurrentPage(1);
renderPage(1);
} catch (error) {
console.error("Error loading PDF:", error);
}
}
};
const renderPage = async (pageNum: number) => {
if (!documentRef.current || !canvasRef.current) return;
try {
const page = await documentRef.current.getPage(pageNum);
const viewport = page.getViewport();
const canvas = canvasRef.current;
canvas.width = viewport.width;
canvas.height = viewport.height;
await workerRef.current.renderPage(page, canvas);
// Draw existing signatures
const ctx = canvas.getContext('2d');
if (ctx && signatures[pageNum]) {
ctx.fillStyle = 'rgba(79, 70, 229, 0.3)';
ctx.fillRect(100, 100, 150, 80);
ctx.fillStyle = '#4f46e5';
ctx.font = '14px Arial';
ctx.fillText('Signature', 120, 150);
}
} catch (error) {
console.error("Error rendering page:", error);
}
};
useEffect(() => {
if (pdfFile && currentPage > 0) {
renderPage(currentPage);
}
}, [currentPage, signatures]);
const handleAddRecipient = () => {
if (newRecipient.name && newRecipient.email) {
const newRec = {
id: Math.random().toString(36),
name: newRecipient.name,
email: newRecipient.email,
signed: false
};
setRecipients([...recipients, newRec]);
setNewRecipient({ name: "", email: "" });
setIsModalOpen(false);
}
};
const handleSignDocument = (recipientId: string) => {
setRecipients(recipients.map(recipient =>
recipient.id === recipientId ? { ...recipient, signed: true } : recipient
));
};
const handleEncryptDocument = async () => {
if (!documentRef.current) return;
try {
await workerRef.current.encryptDocument();
setIsSigned(true);
} catch (error) {
console.error("Error encrypting document:", error);
}
};
const handleDownload = async () => {
if (!documentRef.current) return;
try {
await workerRef.current.downloadDocument();
} catch (error) {
console.error("Error downloading document:", error);
}
};
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isSigningMode || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setSigningPosition({ x, y });
setIsSigningMode(false);
// Add signature to worker
workerRef.current.addSignature(currentPage, { x, y })
.then(signatureId => {
setSignatures(prev => ({
...prev,
[currentPage]: signatureId
}));
});
};
const nextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
const prevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
return (
<div className="min-h-screen bg-background p-4 md:p-8">
<div className="mx-auto max-w-7xl space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight text-foreground">Advanced PDF Signer</h1>
<p className="text-muted-foreground">
Upload, sign, encrypt, and download PDF documents with signature placement
</p>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Left Panel - Controls */}
<div className="space-y-6 lg:col-span-1">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
Upload Document
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div
className="border-2 border-dashed border-border rounded-lg p-8 text-center cursor-pointer hover:bg-muted transition-colors"
onClick={() => fileInputRef.current?.click()}
>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="application/pdf"
onChange={handleFileChange}
/>
<div className="flex flex-col items-center justify-center gap-2">
<Upload className="h-8 w-8 text-muted-foreground" />
<p className="text-muted-foreground">
{pdfFile ? pdfFile.name : "Click to upload PDF"}
</p>
</div>
</div>
{pdfFile && (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Document:</span>
<span className="text-sm text-muted-foreground truncate max-w-[200px]">
{pdfFile.name}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Size:</span>
<span className="text-sm text-muted-foreground">
{(pdfFile.size / 1024 / 1024).toFixed(2)} MB
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Pages:</span>
<span className="text-sm text-muted-foreground">
{totalPages}
</span>
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Recipients
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Button
className="w-full"
onClick={() => setIsModalOpen(true)}
>
<Plus className="mr-2 h-4 w-4" />
Add Recipient
</Button>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{recipients.map((recipient) => (
<TableRow key={recipient.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{recipient.name}</span>
</div>
<p className="text-xs text-muted-foreground">{recipient.email}</p>
</TableCell>
<TableCell>
<Badge variant={recipient.signed ? "default" : "secondary"}>
{recipient.signed ? "Signed" : "Pending"}
</Badge>
</TableCell>
<TableCell>
{!recipient.signed ? (
<Button
size="sm"
onClick={() => handleSignDocument(recipient.id)}
>
Sign
</Button>
) : (
<Check className="h-4 w-4 text-green-500" />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Document Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleEncryptDocument}
disabled={!pdfFile || isSigned}
className="w-full"
>
<Shield className="mr-2 h-4 w-4" />
Encrypt
</Button>
<Button
onClick={handleDownload}
disabled={!pdfFile || !isSigned}
className="w-full"
>
<Download className="mr-2 h-4 w-4" />
Download
</Button>
</div>
{isSigned && (
<div className="rounded-md bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center gap-2">
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
<p className="text-sm font-medium text-green-800 dark:text-green-200">
Document signed and encrypted successfully!
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
{/* Right Panel - PDF Viewer */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Eye className="h-5 w-5" />
Document Preview
</CardTitle>
{pdfFile && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setIsSigningMode(!isSigningMode)}
className={isSigningMode ? "border-primary" : ""}
>
{isSigningMode ? "Cancel Signing" : "Place Signature"}
</Button>
</div>
)}
</div>
</CardHeader>
<CardContent>
{pdfFile ? (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={prevPage}
disabled={currentPage <= 1}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={nextPage}
disabled={currentPage >= totalPages}
>
Next
</Button>
</div>
</div>
<div className="relative border border-border rounded-lg overflow-hidden">
<canvas
ref={canvasRef}
className="w-full max-h-[70vh]"
onClick={handleCanvasClick}
/>
{isSigningMode && (
<div className="absolute inset-0 bg-black/30 flex items-center justify-center">
<div className="bg-white p-4 rounded-lg shadow-lg">
<p className="font-medium">Click where you want to place your signature</p>
</div>
</div>
)}
</div>
{signingPosition && (
<div className="text-sm text-muted-foreground">
Signature placed at: ({Math.round(signingPosition.x)}, {Math.round(signingPosition.y)})
</div>
)}
</div>
) : (
<div className="flex h-[500px] items-center justify-center rounded-lg border border-border bg-muted">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
<p className="mt-2 text-muted-foreground">
Upload a PDF to preview
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
{/* Add Recipient Modal */}
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Add New Recipient</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
value={newRecipient.name}
onChange={(e) => setNewRecipient({...newRecipient, name: e.target.value})}
placeholder="John Doe"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="email"
type="email"
className="pl-10"
value={newRecipient.email}
onChange={(e) => setNewRecipient({...newRecipient, email: e.target.value})}
placeholder="[email protected]"
/>
</div>
</div>
<Button onClick={handleAddRecipient} className="w-full">
Add Recipient
</Button>
</div>
</div>
</Modal>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment