--- phase: 02-marketing-site plan: 02 type: execute wave: 1 depends_on: [] files_modified: - teressa-copeland-homes/lib/contact-mailer.ts - teressa-copeland-homes/lib/contact-action.ts - teressa-copeland-homes/app/_components/ContactSection.tsx - teressa-copeland-homes/app/page.tsx - teressa-copeland-homes/.env.local autonomous: true requirements: - MKTG-03 user_setup: - service: nodemailer_smtp why: "Contact form email delivery to Teressa's inbox" env_vars: - name: CONTACT_EMAIL_USER source: "Teressa's email address (e.g. teressa@teressacopelandhomes.com or Gmail address)" - name: CONTACT_EMAIL_PASS source: "Gmail: Google Account → Security → App Passwords (requires 2FA enabled). Other providers: SMTP password from hosting control panel." - name: CONTACT_SMTP_HOST source: "Gmail: smtp.gmail.com | Namecheap: mail.privateemail.com | Other: check hosting provider docs" - name: CONTACT_SMTP_PORT source: "587 for STARTTLS (recommended) or 465 for SSL" must_haves: truths: - "Visitor can fill in name, email, phone, and message and submit the contact form" - "On success, the form is replaced with 'Thanks! Teressa will be in touch soon.'" - "Submissions with the honeypot field filled are silently discarded (no email sent)" - "Server-side Zod validation rejects missing required fields" - "SMTP credentials are read from env vars — never hardcoded" artifacts: - path: "teressa-copeland-homes/lib/contact-mailer.ts" provides: "Nodemailer transporter + sendContactEmail() function" exports: ["sendContactEmail"] - path: "teressa-copeland-homes/lib/contact-action.ts" provides: "Server Action — validates with Zod, checks honeypot, calls mailer" exports: ["submitContact", "ContactState"] - path: "teressa-copeland-homes/app/_components/ContactSection.tsx" provides: "Client component — useActionState form with honeypot, success swap" key_links: - from: "teressa-copeland-homes/app/_components/ContactSection.tsx" to: "teressa-copeland-homes/lib/contact-action.ts" via: "useActionState(submitContact, initialState)" pattern: "useActionState" - from: "teressa-copeland-homes/lib/contact-action.ts" to: "teressa-copeland-homes/lib/contact-mailer.ts" via: "await sendContactEmail(parsed.data)" pattern: "sendContactEmail" - from: "teressa-copeland-homes/lib/contact-mailer.ts" to: "process.env.CONTACT_EMAIL_USER / CONTACT_EMAIL_PASS" via: "nodemailer.createTransport auth object" pattern: "process\\.env\\.CONTACT" --- Build the contact form: Nodemailer SMTP mailer utility, Server Action with Zod validation and honeypot check, and the client-side form component with success-state swap. Purpose: Visitors can reach Teressa directly from the homepage without any third-party form service. Output: lib/contact-mailer.ts, lib/contact-action.ts, app/_components/ContactSection.tsx wired into page.tsx. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @/Users/ccopeland/temp/red/.planning/phases/02-marketing-site/02-CONTEXT.md @/Users/ccopeland/temp/red/.planning/phases/02-marketing-site/02-RESEARCH.md Key patterns from research: - Use `useActionState` from 'react' (NOT useFormState from react-dom — removed in React 19) - Nodemailer `from` must match CONTACT_EMAIL_USER; use `replyTo` for visitor's email - Honeypot field name="website" with display:none, tabIndex={-1}, autoComplete="off" - Server Action file must have 'use server' directive at top Task 1: Install nodemailer and create mailer + server action teressa-copeland-homes/lib/contact-mailer.ts teressa-copeland-homes/lib/contact-action.ts 1. Install dependencies from the teressa-copeland-homes directory: ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes npm install nodemailer npm install --save-dev @types/nodemailer ``` 2. Create `lib/contact-mailer.ts`: ```typescript import nodemailer from 'nodemailer' const transporter = nodemailer.createTransport({ host: process.env.CONTACT_SMTP_HOST, port: Number(process.env.CONTACT_SMTP_PORT ?? 587), secure: false, // STARTTLS on port 587 auth: { user: process.env.CONTACT_EMAIL_USER, pass: process.env.CONTACT_EMAIL_PASS, }, }) export async function sendContactEmail(data: { name: string email: string phone: string message: string }) { await transporter.sendMail({ from: `"Teressa Copeland Homes Website" <${process.env.CONTACT_EMAIL_USER}>`, replyTo: data.email, to: process.env.CONTACT_EMAIL_USER, subject: `New contact from ${data.name} — Teressa Copeland Homes`, text: `Name: ${data.name}\nEmail: ${data.email}\nPhone: ${data.phone}\n\nMessage:\n${data.message}`, html: `

Name: ${data.name}

Email: ${data.email}

Phone: ${data.phone}

Message:

${data.message.replace(/\n/g, '
')}

`, }) } ``` 3. Create `lib/contact-action.ts`: ```typescript 'use server' import { z } from 'zod' import { sendContactEmail } from './contact-mailer' const ContactSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Valid email address required'), phone: z.string().min(7, 'Phone number is required'), message: z.string().min(10, 'Message must be at least 10 characters'), website: z.string(), // honeypot — must remain empty }) export type ContactState = { status: 'idle' | 'success' | 'error' message?: string } export async function submitContact( _prev: ContactState, formData: FormData, ): Promise { const raw = { name: formData.get('name'), email: formData.get('email'), phone: formData.get('phone'), message: formData.get('message'), website: formData.get('website') ?? '', } const parsed = ContactSchema.safeParse(raw) if (!parsed.success) { return { status: 'error', message: 'Please fill in all required fields correctly.' } } // Honeypot check — if filled, silently succeed (don't reveal rejection to bots) if (parsed.data.website !== '') { return { status: 'success' } } try { await sendContactEmail(parsed.data) return { status: 'success' } } catch (err) { console.error('Contact form email error:', err) return { status: 'error', message: 'Something went wrong. Please try again or call directly.' } } } ``` 4. Add SMTP placeholder vars to `.env.local` (append — do not overwrite existing vars): ``` # Contact form SMTP — fill in before testing email delivery CONTACT_EMAIL_USER=your_email@example.com CONTACT_EMAIL_PASS=your_app_password CONTACT_SMTP_HOST=smtp.gmail.com CONTACT_SMTP_PORT=587 ```
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20 Build succeeds. lib/contact-mailer.ts and lib/contact-action.ts exist with no TypeScript errors. nodemailer appears in package.json dependencies.
Task 2: Build ContactSection client component and wire into page teressa-copeland-homes/app/_components/ContactSection.tsx teressa-copeland-homes/app/page.tsx Create `app/_components/ContactSection.tsx` — 'use client': ```typescript 'use client' import { useActionState } from 'react' import { submitContact, type ContactState } from '@/lib/contact-action' const initialState: ContactState = { status: 'idle' } export function ContactSection() { const [state, action, pending] = useActionState(submitContact, initialState) return (

Get in Touch

Ready to find your Utah home? Teressa is here to help.

{state.status === 'success' ? (

Thanks! Teressa will be in touch soon.

) : (
{/* Honeypot — hidden from humans, bots fill it in */}