From 5842ffc9f363295722741a6f043875bb75b857e4 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Thu, 19 Mar 2026 15:04:45 -0600 Subject: [PATCH] =?UTF-8?q?docs(02-02):=20complete=20contact=20form=20plan?= =?UTF-8?q?=20=E2=80=94=20SUMMARY,=20STATE,=20plans=20tracked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .planning/STATE.md | 2 +- .../phases/02-marketing-site/02-01-PLAN.md | 172 ++++++++++ .../phases/02-marketing-site/02-02-PLAN.md | 307 ++++++++++++++++++ .../phases/02-marketing-site/02-02-SUMMARY.md | 137 ++++++++ .../phases/02-marketing-site/02-03-PLAN.md | 130 ++++++++ 5 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/02-marketing-site/02-01-PLAN.md create mode 100644 .planning/phases/02-marketing-site/02-02-PLAN.md create mode 100644 .planning/phases/02-marketing-site/02-02-SUMMARY.md create mode 100644 .planning/phases/02-marketing-site/02-03-PLAN.md diff --git a/.planning/STATE.md b/.planning/STATE.md index ce03997..fc3d64d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -88,5 +88,5 @@ None yet. ## Session Continuity Last session: 2026-03-19 -Stopped at: Completed 02-01-PLAN.md — Marketing site shell: sticky nav, hero section with next/image, testimonials carousel, listings placeholder, site footer +Stopped at: Completed 02-02-PLAN.md — Contact form: Nodemailer SMTP mailer, Server Action with Zod+honeypot, ContactSection client component Resume file: None diff --git a/.planning/phases/02-marketing-site/02-01-PLAN.md b/.planning/phases/02-marketing-site/02-01-PLAN.md new file mode 100644 index 0000000..197e4f9 --- /dev/null +++ b/.planning/phases/02-marketing-site/02-01-PLAN.md @@ -0,0 +1,172 @@ +--- +phase: 02-marketing-site +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/app/page.tsx + - teressa-copeland-homes/app/_components/SiteNav.tsx + - teressa-copeland-homes/app/_components/HeroSection.tsx + - teressa-copeland-homes/app/_components/TestimonialsSection.tsx + - teressa-copeland-homes/app/_components/ListingsPlaceholder.tsx + - teressa-copeland-homes/app/_components/SiteFooter.tsx + - teressa-copeland-homes/app/globals.css +autonomous: true +requirements: + - MKTG-01 + - MKTG-02 + - MKTG-04 + +must_haves: + truths: + - "Visitor sees a split-panel hero with Teressa's photo on the left and bio copy on the right" + - "Visitor sees an auto-scrolling testimonials carousel that rotates every 5 seconds and pauses on hover" + - "Visitor sees a 'Listings Coming Soon' placeholder section with brand styling" + - "Sticky nav stays visible on scroll and anchor links smooth-scroll to each section" + - "Footer shows Teressa's name, license number (with verify comment), and copyright year" + artifacts: + - path: "teressa-copeland-homes/app/page.tsx" + provides: "Homepage route — composes all section components in correct order" + - path: "teressa-copeland-homes/app/_components/SiteNav.tsx" + provides: "Sticky nav with wordmark, desktop links, mobile hamburger" + - path: "teressa-copeland-homes/app/_components/HeroSection.tsx" + provides: "Split-panel hero with next/image + placeholder copy" + - path: "teressa-copeland-homes/app/_components/TestimonialsSection.tsx" + provides: "Auto-scrolling carousel — useState/useEffect, arrows, dots" + - path: "teressa-copeland-homes/app/_components/ListingsPlaceholder.tsx" + provides: "Listings coming soon section with brand colors" + - path: "teressa-copeland-homes/app/_components/SiteFooter.tsx" + provides: "Footer with name, license number, copyright, nav links" + key_links: + - from: "teressa-copeland-homes/app/page.tsx" + to: "app/_components/*.tsx" + via: "named imports, composed in section order" + pattern: "import.*from.*_components" + - from: "teressa-copeland-homes/app/_components/SiteNav.tsx" + to: "section ids (hero, about, listings, contact)" + via: "href anchor links + CSS scroll-behavior: smooth" + pattern: "href=\"#" + - from: "teressa-copeland-homes/app/_components/TestimonialsSection.tsx" + to: "interval cleanup" + via: "useEffect return clearInterval" + pattern: "clearInterval" +--- + + +Build the static visual shell of the public homepage: sticky nav, hero section, testimonials carousel, listings placeholder, and footer. No external services — pure React/Next.js components styled with brand colors. + +Purpose: Establishes the public face of teressacopelandhomes.com with Teressa's professional brand presence. +Output: Six component files + updated page.tsx and globals.css. + + + +@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md +@/Users/ccopeland/.claude/get-shit-done/templates/summary.md + + + +@/Users/ccopeland/temp/red/.planning/PROJECT.md +@/Users/ccopeland/temp/red/.planning/ROADMAP.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 + +Brand colors: navy #1B2B4B, gold #C9A84C, cream #FAF9F7 +Hero photo: public/red.jpg (already in project) + + + + + + Task 1: Scaffold page route and section components (nav, hero, listings, footer) + + teressa-copeland-homes/app/page.tsx + teressa-copeland-homes/app/_components/SiteNav.tsx + teressa-copeland-homes/app/_components/HeroSection.tsx + teressa-copeland-homes/app/_components/ListingsPlaceholder.tsx + teressa-copeland-homes/app/_components/SiteFooter.tsx + teressa-copeland-homes/app/globals.css + + +1. Add `html { scroll-behavior: smooth; }` and `section[id] { scroll-margin-top: 72px; }` to globals.css. + +2. Create `app/_components/SiteNav.tsx` — 'use client'. Sticky nav (position: sticky, top: 0, z-index: 50, background #1B2B4B, color #FAF9F7). Left: wordmark "Teressa Copeland" bold. Right: desktop anchor links (Home→#hero, About→#about, Listings→#listings, Contact→#contact) hidden on mobile. Mobile: hamburger Menu/X icon from lucide-react toggles a dropdown drawer with the same links. On mobile link click, close the drawer. Check if lucide-react is installed (`grep lucide teressa-copeland-homes/package.json`); if absent, run `npm install lucide-react` first from the teressa-copeland-homes directory. + +3. Create `app/_components/HeroSection.tsx` — server component. Section id="hero". Split-panel layout: left half = `
` with `Teressa Copeland`. Right half = flex column: headline, subheading, bio paragraph, and "Get in Touch" button (href="#contact", background #C9A84C, color #1B2B4B). ALL copy must be wrapped in clearly labelled comments: + ``` + {/* PLACEHOLDER HEADLINE — replace before launch */} + {/* PLACEHOLDER BIO — replace before launch */} + ``` + Sample copy: headline "Your Trusted Utah Real Estate Partner", subheading "Helping Utah families find the home they deserve", bio paragraph (~3 sentences about Teressa's experience, Utah market expertise, commitment to clients). Use next/image for the photo. + +4. Create `app/_components/ListingsPlaceholder.tsx` — server component. Section id="listings", background #1B2B4B, color #FAF9F7. Center-aligned: Home icon (lucide-react) at ~48px, heading "Listings Coming Soon", brief description ("Teressa is actively curating listings in your area. Check back soon or reach out directly."). A "Contact Teressa" link button (href="#contact", border #C9A84C, color #C9A84C). + +5. Create `app/_components/SiteFooter.tsx` — server component. Background #1B2B4B, color #FAF9F7, padding 2rem. Three rows: (1) "Teressa Copeland Homes" bold, (2) license line with comment `{/* TODO: Verify license number before launch — last two chars may be "00" (zeros) or "OO" (letters O) */}` then text "Utah Real Estate License: 14196185-SA00", (3) copyright `© {new Date().getFullYear()} Teressa Copeland Homes. All rights reserved.` Repeated nav links row (same four anchors). + +6. Update `app/page.tsx` to import and render components in order: `` → `` → (TestimonialsSection placeholder) → `` → (ContactSection placeholder — just a `
` stub) → ``. Page is a server component; client islands are the individual components. Remove any existing placeholder content from the Phase 1 dashboard stub if it is in page.tsx (it likely is not — check first). + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20 + + Build succeeds with no TypeScript errors. All six files exist. `npm run dev` serves a page at localhost:3000 that shows the navy nav, hero split panel with photo, listings placeholder section, and footer. + + + + Task 2: Build testimonials carousel component + + teressa-copeland-homes/app/_components/TestimonialsSection.tsx + teressa-copeland-homes/app/page.tsx + + +Create `app/_components/TestimonialsSection.tsx` — 'use client'. + +State: `useState(0)` for activeIndex, `useState(false)` for paused. `useRef` for interval ID. + +useEffect: When not paused, set a 5000ms interval that advances activeIndex via `setActive(i => (i + 1) % TESTIMONIALS.length)`. Cleanup MUST return `clearInterval(intervalRef.current)` — prevents memory leak on re-render. + +Handlers: `prev()` decrements index with wraparound. `next()` increments. Dot buttons call `setActive(i)` directly. + +Mouse events: `onMouseEnter` sets paused=true, `onMouseLeave` sets paused=false on the outer section. + +TESTIMONIALS array — 5 entries, clearly commented as placeholders: +``` +// PLACEHOLDER TESTIMONIALS — replace with real client reviews before launch +{ quote: "Working with Teressa made buying our first home in Salt Lake County feel effortless. She guided us through every step with patience and expertise.", name: "Sarah Mitchell" }, +{ quote: "Teressa sold our Provo home in under a week — above asking price. Her knowledge of the Utah market is exceptional.", name: "James & Karen Olsen" }, +{ quote: "As first-time buyers, we had so many questions. Teressa answered every one and found us the perfect home in our budget.", name: "Tyler Reeves" }, +{ quote: "Relocating from out of state is stressful, but Teressa made our transition to Utah smooth and seamless.", name: "Michelle Torres" }, +{ quote: "Teressa's negotiating skills saved us thousands. We couldn't be happier with our new home in Herriman.", name: "David & Pam Christensen" }, +``` + +Layout: Section id="about", background #FAF9F7, padding generous (4rem 2rem). Center-aligned. Gold quote mark decorative. Blockquote with quote text, em dash + name attribution. Below: left ChevronLeft arrow button + right ChevronRight arrow button (from lucide-react). Below arrows: dot row — one small circle per testimonial, full opacity for active, 0.3 opacity for inactive. + +Wire into page.tsx: replace the TestimonialsSection placeholder with ``. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20 + + Build succeeds. TestimonialsSection renders in correct position between hero and listings on the homepage. Five placeholder testimonials cycle every 5 seconds. Arrow and dot controls are present. + + + + + +- `npm run build` exits 0 with no TypeScript errors +- `app/page.tsx` renders sections in correct order: SiteNav → HeroSection → TestimonialsSection → ListingsPlaceholder → ContactSection stub → SiteFooter +- All placeholder copy has clear `// PLACEHOLDER` comments +- Footer license number has `// TODO: Verify` comment +- No link to `/agent/login` anywhere on the public page + + + +- Hero section shows split panel with photo on left, bio copy + "Get in Touch" CTA on right +- Testimonials rotate automatically; arrow and dot controls work; hover pauses rotation +- Listings placeholder section is visible with brand navy background +- Sticky nav stays fixed on scroll; anchor links reference correct section IDs +- Footer shows license number with verification comment +- Build passes with zero errors + + + +After completion, create `/Users/ccopeland/temp/red/.planning/phases/02-marketing-site/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-marketing-site/02-02-PLAN.md b/.planning/phases/02-marketing-site/02-02-PLAN.md new file mode 100644 index 0000000..6c35d7d --- /dev/null +++ b/.planning/phases/02-marketing-site/02-02-PLAN.md @@ -0,0 +1,307 @@ +--- +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 */} + + + + + +