diff --git a/teressa-copeland-homes/package-lock.json b/teressa-copeland-homes/package-lock.json index a268281..dccdbf3 100644 --- a/teressa-copeland-homes/package-lock.json +++ b/teressa-copeland-homes/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "dependencies": { "@cantoo/pdf-lib": "^2.6.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/utilities": "^3.2.2", "@vercel/blob": "^2.3.1", "adm-zip": "^0.5.16", "bcryptjs": "^3.0.3", @@ -597,6 +599,45 @@ "tslib": ">=2" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", diff --git a/teressa-copeland-homes/package.json b/teressa-copeland-homes/package.json index 384b587..fdfc4bb 100644 --- a/teressa-copeland-homes/package.json +++ b/teressa-copeland-homes/package.json @@ -16,6 +16,8 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/utilities": "^3.2.2", "@vercel/blob": "^2.3.1", "adm-zip": "^0.5.16", "bcryptjs": "^3.0.3", diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx new file mode 100644 index 0000000..a640a8d --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx @@ -0,0 +1,321 @@ +'use client'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + DndContext, + useDraggable, + useDroppable, + DragOverlay, + type DragEndEvent, +} from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import type { SignatureFieldData } from '@/lib/db/schema'; + +interface PageInfo { + originalWidth: number; + originalHeight: number; + width: number; + height: number; + scale: number; +} + +// Screen (DOM) → PDF user space. Y-axis flip required. +// DOM: Y=0 at top, increases downward +// PDF: Y=0 at bottom, increases upward +function screenToPdfCoords( + screenX: number, + screenY: number, + containerRect: DOMRect, + pageInfo: PageInfo, +) { + // Use containerRect dimensions (current rendered size) not stale pageInfo + const renderedW = containerRect.width; + const renderedH = containerRect.height; + const pdfX = (screenX / renderedW) * pageInfo.originalWidth; + const pdfY = ((renderedH - screenY) / renderedH) * pageInfo.originalHeight; + return { x: pdfX, y: pdfY }; +} + +// PDF user space → screen (for rendering stored fields) +function pdfToScreenCoords( + pdfX: number, + pdfY: number, + containerRect: DOMRect, + pageInfo: PageInfo, +) { + const renderedW = containerRect.width; + const renderedH = containerRect.height; + const left = (pdfX / pageInfo.originalWidth) * renderedW; + // top is measured from DOM top; pdfY is from PDF bottom — reverse flip + const top = renderedH - (pdfY / pageInfo.originalHeight) * renderedH; + return { left, top }; +} + +async function persistFields(docId: string, fields: SignatureFieldData[]) { + try { + await fetch(`/api/documents/${docId}/fields`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields), + }); + } catch (e) { + console.error('Failed to persist fields:', e); + } +} + +// Draggable token in the palette +function DraggableToken({ id }: { id: string }) { + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id }); + const style: React.CSSProperties = { + transform: CSS.Translate.toString(transform), + opacity: isDragging ? 0.4 : 1, + cursor: 'grab', + padding: '6px 12px', + border: '2px dashed #2563eb', + borderRadius: '4px', + background: 'rgba(37,99,235,0.08)', + color: '#2563eb', + fontSize: '13px', + fontWeight: 600, + userSelect: 'none', + touchAction: 'none', + }; + + return ( +