- Create 02-02-SUMMARY.md: contact form outcomes, decisions, deviations - Update STATE.md: position, decisions (nodemailer v7 pin, useActionState React 19, honeypot pattern) - Add 02-01-PLAN.md, 02-02-PLAN.md, 02-03-PLAN.md to version control - Mark MKTG-03 requirement complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-marketing-site | 02 | execute | 1 |
|
true |
|
|
|
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.
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>
@/Users/ccopeland/temp/red/.planning/phases/02-marketing-site/02-CONTEXT.md @/Users/ccopeland/temp/red/.planning/phases/02-marketing-site/02-RESEARCH.mdKey patterns from research:
- Use
useActionStatefrom 'react' (NOT useFormState from react-dom — removed in React 19) - Nodemailer
frommust match CONTACT_EMAIL_USER; usereplyTofor visitor's email - Honeypot field name="website" with display:none, tabIndex={-1}, autoComplete="off"
- Server Action file must have 'use server' directive at top
-
Create
lib/contact-mailer.ts: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: `<p><strong>Name:</strong> ${data.name}</p> <p><strong>Email:</strong> <a href="mailto:${data.email}">${data.email}</a></p> <p><strong>Phone:</strong> ${data.phone}</p> <p><strong>Message:</strong></p> <p>${data.message.replace(/\n/g, '<br/>')}</p>`, }) } -
Create
lib/contact-action.ts:'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<ContactState> { 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.' } } } -
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
'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 (
<section id="contact" style={{ backgroundColor: '#FAF9F7', padding: '4rem 2rem' }}>
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2 style={{ color: '#1B2B4B', marginBottom: '0.5rem' }}>Get in Touch</h2>
<p style={{ color: '#555', marginBottom: '2rem' }}>
Ready to find your Utah home? Teressa is here to help.
</p>
{state.status === 'success' ? (
<div role="status" style={{ padding: '2rem', backgroundColor: '#e8f5e9', borderRadius: '8px', color: '#2e7d32', textAlign: 'center' }}>
<p style={{ fontSize: '1.1rem', fontWeight: 600 }}>Thanks! Teressa will be in touch soon.</p>
</div>
) : (
<form action={action} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{/* Honeypot — hidden from humans, bots fill it in */}
<input
name="website"
type="text"
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
/>
<input name="name" type="text" required placeholder="Your full name" />
<input name="email" type="email" required placeholder="Email address" />
<input name="phone" type="tel" required placeholder="Phone number" />
<textarea name="message" required placeholder="How can Teressa help you?" rows={5} />
{state.status === 'error' && (
<p role="alert" style={{ color: '#c62828' }}>{state.message}</p>
)}
<button
type="submit"
disabled={pending}
style={{
backgroundColor: '#C9A84C',
color: '#1B2B4B',
padding: '0.75rem 2rem',
border: 'none',
borderRadius: '4px',
fontWeight: 700,
cursor: pending ? 'not-allowed' : 'pointer',
opacity: pending ? 0.7 : 1,
}}
>
{pending ? 'Sending...' : 'Send Message'}
</button>
</form>
)}
</div>
</section>
)
}
Update app/page.tsx: replace the <section id="contact" /> stub with <ContactSection />. Import ContactSection at the top. Ensure all section imports are present and in correct order: SiteNav, HeroSection, TestimonialsSection (from plan 01), ListingsPlaceholder, ContactSection, SiteFooter.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20
Build succeeds. ContactSection renders a form with name, email, phone, message fields and a gold "Send Message" button. Form wires to submitContact server action via useActionState.
<success_criteria>
- Contact form renders with all four required fields
- Submitting with the honeypot filled returns success without sending email
- Missing required fields return an error message
- On real success, form is replaced by thank-you message
- Build passes with zero TypeScript errors </success_criteria>