From add09cf84ad8bd67595f8800d1e49fdd11530b22 Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Fri, 14 Aug 2026 07:44:36 +0200 Subject: [PATCH] feat(contact): wire up the resume contact form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ResumeContact block previously rendered only a title/caption with a commented-out mail link — no actual form existed. - Add ContactForm.client.tsx: name/email/message inputs plus a hidden honeypot field, driven by useActionState against a server action. - Add submitContactForm server action: validates input with zod, rejects silently on a filled honeypot, looks up the recipient email from GlobalUserSettings, and sends via the existing useSendAdapter through payload.sendEmail, with the submitter set as replyTo. Errors are logged server-side and reported to the client as a generic message so upstream details never leak. - Wire ContactForm into ResumeContactBlockClientRenderer in place of the commented-out button. --- .../Renderer/ContactForm.client.tsx | 138 ++++++++++++++++ .../Renderer/Renderer.client.tsx | 10 +- src/lib/actions/submitContactForm.test.ts | 152 ++++++++++++++++++ src/lib/actions/submitContactForm.ts | 109 +++++++++++++ 4 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 src/blocks/ResumeContactBlock/Renderer/ContactForm.client.tsx create mode 100644 src/lib/actions/submitContactForm.test.ts create mode 100644 src/lib/actions/submitContactForm.ts diff --git a/src/blocks/ResumeContactBlock/Renderer/ContactForm.client.tsx b/src/blocks/ResumeContactBlock/Renderer/ContactForm.client.tsx new file mode 100644 index 0000000..e08251f --- /dev/null +++ b/src/blocks/ResumeContactBlock/Renderer/ContactForm.client.tsx @@ -0,0 +1,138 @@ +'use client' + +import { useActionState, useEffect, useId, useRef } from 'react' + +import { Button } from '@/components/Button' +import { type ContactFormState, submitContactForm } from '@/lib/actions/submitContactForm' +import { cn } from '@/lib/cn' + +const initialState: ContactFormState = { + status: 'idle', +} + +const inputStyles = cn( + 'w-full rounded-none border border-border bg-transparent px-4 py-3', + 'text-base placeholder:text-muted-foreground', + 'outline-none transition-colors', + 'focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30', + 'aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20', +) + +export const ContactForm = () => { + const [state, formAction, isPending] = useActionState(submitContactForm, initialState) + const formRef = useRef(null) + const nameId = useId() + const emailId = useId() + const messageId = useId() + const companyId = useId() + + useEffect(() => { + if (state.status === 'success') { + formRef.current?.reset() + } + }, [ + state.status, + ]) + + return ( +
+
+ + + {state.fieldErrors?.name && ( +

+ {state.fieldErrors.name} +

+ )} +
+ +
+ + + {state.fieldErrors?.email && ( +

+ {state.fieldErrors.email} +

+ )} +
+ +
+ +