Created
September 22, 2025 11:41
-
-
Save dexit/0d4eae02efc2e194b6b92d1b93f629c3 to your computer and use it in GitHub Desktop.
another component
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
| "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