From de195a3e8088bcd35d6812b56dc5cc071ddef77f Mon Sep 17 00:00:00 2001
From: Chandler Copeland
Date: Sat, 21 Mar 2026 15:36:47 -0600
Subject: [PATCH] feat(12-02): PreparePanel preview state, button, gating,
modal, and DocumentPageClient wiring
- Add previewToken/onPreviewTokenChange props to PreparePanel (lifted to DocumentPageClient)
- Add handlePreview async function fetching POST /api/documents/[id]/preview
- Add Preview button (gray-700) before Send button; text cycles Preview/Preview again
- Gate Send button on previewToken === null (requires fresh preview before send)
- Wrap TextFillForm onChange to reset previewToken on text fill changes
- Render PreviewModal conditionally when showPreview && previewBytes
- Create DocumentPageClient.tsx: holds previewToken state, passes reset callback to both FieldPlacer (via PdfViewerWrapper/PdfViewer) and PreparePanel
- Update PdfViewerWrapper and PdfViewer to accept and forward onFieldsChanged prop
- Update page.tsx to use DocumentPageClient instead of direct PreparePanel/PdfViewerWrapper siblings
---
.../_components/DocumentPageClient.tsx | 51 ++++++++++++++++++
.../[docId]/_components/PdfViewer.tsx | 4 +-
.../[docId]/_components/PdfViewerWrapper.tsx | 4 +-
.../[docId]/_components/PreparePanel.tsx | 54 +++++++++++++++++--
.../(protected)/documents/[docId]/page.tsx | 28 ++++------
5 files changed, 116 insertions(+), 25 deletions(-)
create mode 100644 teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
new file mode 100644
index 0000000..8f5bf75
--- /dev/null
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
@@ -0,0 +1,51 @@
+'use client';
+import { useState, useCallback } from 'react';
+import { PdfViewerWrapper } from './PdfViewerWrapper';
+import { PreparePanel } from './PreparePanel';
+
+interface DocumentPageClientProps {
+ docId: string;
+ docStatus: string;
+ defaultEmail: string;
+ clientName: string;
+ agentDownloadUrl?: string | null;
+ signedAt?: Date | null;
+ clientPropertyAddress?: string | null;
+}
+
+export function DocumentPageClient({
+ docId,
+ docStatus,
+ defaultEmail,
+ clientName,
+ agentDownloadUrl,
+ signedAt,
+ clientPropertyAddress,
+}: DocumentPageClientProps) {
+ const [previewToken, setPreviewToken] = useState(null);
+
+ const handleFieldsChanged = useCallback(() => {
+ setPreviewToken(null);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
index d47cd20..70dad79 100644
--- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
@@ -19,7 +19,7 @@ interface PageInfo {
scale: number;
}
-export function PdfViewer({ docId, docStatus }: { docId: string; docStatus?: string }) {
+export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void }) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1.0);
@@ -69,7 +69,7 @@ export function PdfViewer({ docId, docStatus }: { docId: string; docStatus?: str
{/* PDF canvas wrapped in FieldPlacer for drag-and-drop field placement */}
-
+
setNumPages(numPages)}
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
index 7fcc29f..5862ca4 100644
--- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
@@ -3,6 +3,6 @@ import dynamic from 'next/dynamic';
const PdfViewer = dynamic(() => import('./PdfViewer').then(m => m.PdfViewer), { ssr: false });
-export function PdfViewerWrapper({ docId, docStatus }: { docId: string; docStatus?: string }) {
- return ;
+export function PdfViewerWrapper({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void }) {
+ return ;
}
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
index e4bb0b8..2ede9f0 100644
--- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { TextFillForm } from './TextFillForm';
+import { PreviewModal } from './PreviewModal';
interface PreparePanelProps {
docId: string;
@@ -11,6 +12,8 @@ interface PreparePanelProps {
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
+ previewToken: string | null;
+ onPreviewTokenChange: (token: string | null) => void;
}
function parseEmails(raw: string | undefined): string[] {
@@ -21,7 +24,7 @@ function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
-export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, agentDownloadUrl, signedAt, clientPropertyAddress }: PreparePanelProps) {
+export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, agentDownloadUrl, signedAt, clientPropertyAddress, previewToken, onPreviewTokenChange }: PreparePanelProps) {
const router = useRouter();
const [recipients, setRecipients] = useState(defaultEmail ?? '');
// Sync if defaultEmail arrives after initial render (streaming / hydration timing)
@@ -33,6 +36,8 @@ export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, a
);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
+ const [previewBytes, setPreviewBytes] = useState(null);
+ const [showPreview, setShowPreview] = useState(false);
if (currentStatus === 'Signed') {
return (
@@ -84,6 +89,36 @@ export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, a
);
}
+ function handleTextFillChange(data: Record) {
+ setTextFillData(data);
+ onPreviewTokenChange(null);
+ }
+
+ async function handlePreview() {
+ setLoading(true);
+ setResult(null);
+ try {
+ const res = await fetch(`/api/documents/${docId}/preview`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ textFillData }),
+ });
+ if (res.ok) {
+ const bytes = await res.arrayBuffer();
+ setPreviewBytes(bytes);
+ onPreviewTokenChange(Date.now().toString());
+ setShowPreview(true);
+ } else {
+ const err = await res.json().catch(() => ({ error: 'Preview failed' }));
+ setResult({ ok: false, message: err.message ?? err.error ?? 'Preview failed' });
+ }
+ } catch (e) {
+ setResult({ ok: false, message: String(e) });
+ } finally {
+ setLoading(false);
+ }
+ }
+
async function handlePrepare() {
setLoading(true);
setResult(null);
@@ -152,14 +187,23 @@ export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, a
+
+
)}
+
+ {showPreview && previewBytes && (
+ setShowPreview(false)} />
+ )}
);
}
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
index ba4e068..133543d 100644
--- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
@@ -4,8 +4,7 @@ import { db } from '@/lib/db';
import { documents, clients } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import Link from 'next/link';
-import { PdfViewerWrapper } from './_components/PdfViewerWrapper';
-import { PreparePanel } from './_components/PreparePanel';
+import { DocumentPageClient } from './_components/DocumentPageClient';
import { createAgentDownloadToken } from '@/lib/signing/token';
export default async function DocumentPage({
@@ -53,22 +52,15 @@ export default async function DocumentPage({
-
+
);
}