From f383f914459ef5fb0c58f99398123a947b2a7e15 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 14:02:51 -0600 Subject: [PATCH] feat(11-01): AgentSignaturePanel, profile page, PortalNav link, FieldPlacer token - Create AgentSignaturePanel.tsx with signature_pad canvas, save/update/thumbnail flow - Create /portal/profile page (server component fetching agentSignatureData) - Add Profile link to PortalNav navLinks array - Add red 'Agent Signature' token to FieldPlacer PALETTE_TOKENS (6th entry) --- .../[docId]/_components/FieldPlacer.tsx | 11 +- .../app/portal/(protected)/profile/page.tsx | 31 +++++ .../_components/AgentSignaturePanel.tsx | 112 ++++++++++++++++++ .../src/app/portal/_components/PortalNav.tsx | 76 ++++++------ 4 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx create mode 100644 teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx 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 index 7465543..574e4e7 100644 --- 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 @@ -68,11 +68,12 @@ async function persistFields(docId: string, fields: SignatureFieldData[]) { // Token color palette — each maps to a SignatureFieldType const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [ - { id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue - { id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple - { id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green - { id: 'date', label: 'Date', color: '#d97706' }, // amber - { id: 'text', label: 'Text', color: '#64748b' }, // slate + { id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue + { id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple + { id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green + { id: 'date', label: 'Date', color: '#d97706' }, // amber + { id: 'text', label: 'Text', color: '#64748b' }, // slate + { id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red ]; // Draggable token in the palette diff --git a/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx b/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx new file mode 100644 index 0000000..ae319ea --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx @@ -0,0 +1,31 @@ +import { auth } from '@/lib/auth'; +import { redirect } from 'next/navigation'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel'; + +export default async function ProfilePage() { + const session = await auth(); + if (!session?.user?.id) redirect('/agent/login'); + + const user = await db.query.users.findFirst({ + where: eq(users.id, session.user.id), + columns: { agentSignatureData: true }, + }); + + return ( +
+

Profile

+
+
+

Agent Signature

+

+ Draw your signature once. It will be embedded in any "Agent Signature" fields when you prepare a document. +

+
+ +
+
+ ); +} diff --git a/teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx b/teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx new file mode 100644 index 0000000..861822e --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx @@ -0,0 +1,112 @@ +'use client'; +import { useEffect, useRef, useState } from 'react'; +import SignaturePad from 'signature_pad'; + +interface AgentSignaturePanelProps { + initialData: string | null; +} + +export function AgentSignaturePanel({ initialData }: AgentSignaturePanelProps) { + const [savedData, setSavedData] = useState(initialData); + const [isDrawing, setIsDrawing] = useState(!initialData); + const canvasRef = useRef(null); + const sigPadRef = useRef(null); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isDrawing || !canvasRef.current) return; + const canvas = canvasRef.current; + const ratio = Math.max(window.devicePixelRatio || 1, 1); + canvas.width = canvas.offsetWidth * ratio; + canvas.height = canvas.offsetHeight * ratio; + canvas.getContext('2d')?.scale(ratio, ratio); + sigPadRef.current = new SignaturePad(canvas, { + backgroundColor: 'rgba(0,0,0,0)', + penColor: '#1B2B4B', + }); + return () => sigPadRef.current?.off(); + }, [isDrawing]); + + async function handleSave() { + if (!sigPadRef.current || sigPadRef.current.isEmpty()) { + setError('Please draw your signature first'); + return; + } + const dataURL = sigPadRef.current.toDataURL('image/png'); + setSaving(true); + setError(null); + try { + const res = await fetch('/api/agent/signature', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataURL }), + }); + if (res.ok) { + setSavedData(dataURL); + setIsDrawing(false); + } else { + const data = await res.json().catch(() => ({ error: 'Save failed' })); + setError(data.error ?? 'Save failed'); + } + } catch { + setError('Network error — please try again'); + } finally { + setSaving(false); + } + } + + if (!isDrawing && savedData) { + return ( +
+

Your saved signature:

+ {/* eslint-disable-next-line @next/next/no-img-element */} + Saved agent signature + +
+ ); + } + + return ( +
+ +
+ + + {savedData && ( + + )} +
+ {error &&

{error}

} +
+ ); +} diff --git a/teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx b/teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx index 149f523..3889985 100644 --- a/teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx +++ b/teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx @@ -4,52 +4,58 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { LogoutButton } from "@/components/ui/LogoutButton"; -interface PortalNavProps { - userEmail: string; -} - const navLinks = [ { href: "/portal/dashboard", label: "Dashboard" }, { href: "/portal/clients", label: "Clients" }, + { href: "/portal/profile", label: "Profile" }, ]; -export function PortalNav({ userEmail }: PortalNavProps) { +export function PortalNav({ userEmail }: { userEmail: string }) { const pathname = usePathname(); return ( -