From 0cdf3e5f7d34feb0e54de6bc171839647a59358f Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Thu, 19 Mar 2026 14:50:36 -0600 Subject: [PATCH] docs(02): research phase - marketing site Research covering Next.js App Router patterns, Nodemailer SMTP contact form, pure-React testimonials carousel, honeypot spam protection, and React 19 useActionState API for the public marketing homepage phase. Co-Authored-By: Claude Sonnet 4.6 --- .../phases/02-marketing-site/02-RESEARCH.md | 583 ++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 .planning/phases/02-marketing-site/02-RESEARCH.md diff --git a/.planning/phases/02-marketing-site/02-RESEARCH.md b/.planning/phases/02-marketing-site/02-RESEARCH.md new file mode 100644 index 0000000..881d0a9 --- /dev/null +++ b/.planning/phases/02-marketing-site/02-RESEARCH.md @@ -0,0 +1,583 @@ +# Phase 2: Marketing Site - Research + +**Researched:** 2026-03-19 +**Domain:** Next.js 16 App Router — public marketing page, Nodemailer SMTP contact form, carousel UI +**Confidence:** HIGH + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Hero section** +- Layout: Split panel — photo on the left, text content on the right (mirrors the login page feel) +- Tone: Professional and polished — credentials, experience, trust-building (not casual) +- CTA button: "Get in Touch" — smooth-scrolls to the contact form section +- Copy: Claude writes the headline, subheading, and bio (professional Utah real estate agent framing). Wrap text in clearly commented placeholders so Teressa can swap in her own words before launch + +**Testimonials** +- Display: Auto-scrolling carousel — rotates every 5 seconds, pauses on hover +- Navigation: Arrow controls and dot indicators so visitors can also advance manually +- Content: Claude writes 4–5 sample testimonials with realistic Utah buyer/seller scenarios and placeholder full names (e.g., "Sarah Mitchell"). Clearly commented so real ones can be swapped in +- Attribution: Quote + full name only (no city, no photo) + +**Contact form** +- Delivery: Email notification to Teressa via Nodemailer + SMTP (no database storage in this phase) +- SMTP credentials: `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` environment variables — never hardcoded +- Success state: Inline — the form is replaced by a thank-you message ("Thanks! Teressa will be in touch soon.") when submission succeeds +- Spam protection: Honeypot hidden field — bots fill it in, humans never see it; submissions with the honeypot filled are silently discarded +- Validation: Required fields are name, email, phone, and message — validated server-side in the form action + +**Page structure** +- Section order (top to bottom): Sticky nav → Hero → Testimonials → Listings placeholder → Contact form → Footer +- Sticky nav content: Teressa's name as wordmark logo (left), anchor links on the right — Home, About, Listings, Contact — smooth-scroll to each section +- Listings placeholder: A tasteful "Listings Coming Soon" section with a brief description — no real data, just visual presence +- Agent portal: No link anywhere on the public site — Teressa navigates to `/agent/login` directly via URL +- Footer: Name + Utah real estate license number (`14196185-SA00` — NOTE: verify the last two characters, may be letters O or zeros) + copyright year + repeated nav links + +**Placeholder assets** +- Hero photo: reuse `public/red.jpg` from Phase 1 (already in the project) +- Listings placeholder: use a tasteful stock-style image or a solid brand-colored background with an icon — no real property photos needed +- Background section breaks: subtle texture or light gradient using brand cream/navy — no photography required for non-hero sections +- Icons: use Lucide React (already likely available) for any UI icons (phone, email, map pin for contact section) + +### Claude's Discretion +- Exact hero photo crop/focal point and overlay gradient (if any) +- Mobile nav pattern (hamburger menu vs collapsed links) +- Exact card styling, shadow depth, border radius +- Animation timing beyond the 5-second carousel interval +- Exact color weights within the brand palette already established (navy `#1B2B4B`, gold `#C9A84C`, cream `#FAF9F7`) + +### Deferred Ideas (OUT OF SCOPE) +- None — discussion stayed within phase scope + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| MKTG-01 | Visitor sees a hero section with Teressa's professional photo and warm introductory bio | Split-panel layout pattern, Next.js Image component, `public/red.jpg` already in project | +| MKTG-02 | Visitor sees a "listings coming soon" placeholder section (full WFRMLS integration deferred to v2) | Static section component, brand color background + Lucide icon | +| MKTG-03 | Visitor can submit a contact form with name, email, phone, and message | Next.js Server Action, Nodemailer + SMTP, honeypot pattern, server-side Zod validation | +| MKTG-04 | Visitor sees a testimonials section with client reviews on the homepage | Pure React carousel (useState + useEffect interval), no external carousel library needed | + + +--- + +## Summary + +This phase builds a single public-facing homepage for Teressa Copeland Homes on top of the Next.js 16 + Tailwind CSS 4 stack established in Phase 1. The page is a single route (`app/page.tsx`) composed of full-width section components: sticky nav, hero, testimonials carousel, listings placeholder, contact form, and footer. No new major dependencies are required — Nodemailer is the only addition for contact form email delivery. + +The carousel can be built with zero external libraries using React `useState` / `useEffect` — a 5-second auto-advance with hover-pause, arrow buttons, and dot indicators is straightforward in ~60 lines of client component code. Reaching for a carousel library like Embla or Swiper would add unnecessary bundle weight for a single simple use case. + +The contact form uses a Next.js Server Action (no API route needed) with Zod validation already in the project, Nodemailer for SMTP delivery, and a honeypot hidden field for bot protection. No database writes occur in this phase — the form is fire-and-forget email only. + +**Primary recommendation:** Build everything as Next.js App Router server components with minimal client component islands (carousel, mobile nav toggle, form state). Keep the public page route at `app/page.tsx` and colocate section components under `app/_components/` or `components/marketing/`. + +--- + +## Standard Stack + +### Core (already in project — no new installs for UI) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Next.js | 16.2.0 | App Router, Server Actions, Image optimization | Already installed; Phase 1 foundation | +| React | 19.2.4 | Component model | Already installed | +| Tailwind CSS | ^4 | Utility-first styling | Already installed; brand colors via inline styles or CSS vars | +| Zod | ^4.3.6 | Server-side form validation | Already installed; used in Phase 1 auth | + +### New Dependency — Contact Form Email + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| nodemailer | ^6.9.x | SMTP email delivery from Server Action | De facto Node.js SMTP library; well-maintained; zero vendor lock-in | +| @types/nodemailer | ^6.4.x | TypeScript types | Required for TS strict mode | + +### Supporting (already available) + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| lucide-react | latest | Icon set (phone, mail, map-pin, chevron, etc.) | Confirmed in CONTEXT.md; check if installed or add | + +**Check if lucide-react is already installed:** +```bash +grep lucide /Users/ccopeland/temp/red/teressa-copeland-homes/package.json +``` +If absent, add it. + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Hand-rolled carousel | Embla Carousel or Swiper | Overkill for a single 4-5 item testimonials carousel; adds bundle weight | +| Nodemailer | Resend SDK, SendGrid | Adds vendor dependency; Nodemailer works with any SMTP provider already owned by Teressa | +| Server Action form | API Route (`app/api/contact/route.ts`) | Server Actions are simpler, no fetch boilerplate; use progressive enhancement | + +**Installation (new packages only):** +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes +npm install nodemailer +npm install --save-dev @types/nodemailer +# If lucide-react not present: +npm install lucide-react +``` + +--- + +## Architecture Patterns + +### Recommended Project Structure + +``` +teressa-copeland-homes/ +├── app/ +│ ├── page.tsx # Public homepage — server component, composes sections +│ ├── layout.tsx # Root layout (already exists from Phase 1) +│ └── _components/ # Marketing page section components (colocated) +│ ├── SiteNav.tsx # Sticky nav — client component (mobile toggle state) +│ ├── HeroSection.tsx # Server component +│ ├── TestimonialsSection.tsx # Client component (carousel state + interval) +│ ├── ListingsPlaceholder.tsx # Server component +│ ├── ContactSection.tsx # Client component (form state, submission) +│ └── SiteFooter.tsx # Server component +├── lib/ +│ ├── contact-mailer.ts # Nodemailer transporter + sendContactEmail() +│ └── contact-action.ts # Server Action — validates, calls mailer, returns state +└── public/ + └── red.jpg # Hero photo (already exists) +``` + +**Why `app/_components/`:** Next.js treats files prefixed with `_` as private — they're not treated as routes. Keeps section components colocated with the page without polluting the route tree. + +**Alternative:** `components/marketing/` at project root. Either works; pick one and be consistent. + +### Pattern 1: Server Action for Contact Form (No API Route) + +**What:** Form submits to a Server Action; action validates with Zod, sends email via Nodemailer, returns typed state. +**When to use:** Any form in Next.js App Router that doesn't need a public REST endpoint. + +```typescript +// 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 required'), + phone: z.string().min(7, 'Phone is required'), + message: z.string().min(10, 'Message is required'), + // Honeypot — must be empty + website: z.string().max(0), +}) + +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'), // honeypot + } + + const parsed = ContactSchema.safeParse(raw) + if (!parsed.success) { + return { status: 'error', message: 'Please fill in all required fields.' } + } + + // Honeypot filled — silently succeed (don't tell the bot it failed) + if (parsed.data.website !== '') { + return { status: 'success' } + } + + try { + await sendContactEmail(parsed.data) + return { status: 'success' } + } catch { + return { status: 'error', message: 'Something went wrong. Please try again.' } + } +} +``` + +### Pattern 2: Nodemailer SMTP Transporter + +**What:** Singleton transporter initialized from env vars; called from Server Action. +**When to use:** Any server-side email send in Node.js/Next.js. + +```typescript +// lib/contact-mailer.ts +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + host: process.env.CONTACT_SMTP_HOST, // e.g., smtp.gmail.com + port: Number(process.env.CONTACT_SMTP_PORT ?? 587), + secure: false, // true for 465, false for 587 (STARTTLS) + 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: `"${data.name}" <${process.env.CONTACT_EMAIL_USER}>`, + replyTo: data.email, + to: process.env.CONTACT_EMAIL_USER, // Teressa's inbox + subject: `New contact from ${data.name} — Teressa Copeland Homes`, + text: `Name: ${data.name}\nEmail: ${data.email}\nPhone: ${data.phone}\n\n${data.message}`, + html: `

Name: ${data.name}

+

Email: ${data.email}

+

Phone: ${data.phone}

+

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

`, + }) +} +``` + +### Pattern 3: Pure React Testimonials Carousel + +**What:** Client component with `useState` (activeIndex), `useEffect` (5-second interval), pause on hover. +**When to use:** Simple auto-advance carousel with ≤ 10 items — no library needed. + +```typescript +// app/_components/TestimonialsSection.tsx +'use client' +import { useState, useEffect, useRef } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' + +const TESTIMONIALS = [ + // PLACEHOLDER — replace with real client testimonials before launch + { quote: "Working with Teressa made buying our first home in Salt Lake County seamless...", name: "Sarah Mitchell" }, + { quote: "Teressa sold our Provo home in under a week above asking price...", name: "James & Karen Olsen" }, + // add 3-5 total +] + +export function TestimonialsSection() { + const [active, setActive] = useState(0) + const [paused, setPaused] = useState(false) + const intervalRef = useRef | null>(null) + + useEffect(() => { + if (paused) return + intervalRef.current = setInterval(() => { + setActive(i => (i + 1) % TESTIMONIALS.length) + }, 5000) + return () => { if (intervalRef.current) clearInterval(intervalRef.current) } + }, [paused]) + + const prev = () => setActive(i => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length) + const next = () => setActive(i => (i + 1) % TESTIMONIALS.length) + + return ( +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > + {/* carousel content */} +
"{TESTIMONIALS[active].quote}"
+

— {TESTIMONIALS[active].name}

+ + +
+ {TESTIMONIALS.map((_, i) => ( +
+
+ ) +} +``` + +### Pattern 4: Smooth-Scroll Anchor Navigation + +**What:** Pure CSS + HTML `id` anchors; no JavaScript scroll library needed. +**When to use:** Single-page anchor navigation. + +```typescript +// CSS approach — add to globals.css +// html { scroll-behavior: smooth; } + +// Nav links +Home +About +Listings +Contact + +// Section targets +
...
+
...
// testimonials anchors here per section order +
...
+
...
+``` + +### Pattern 5: Contact Form with useActionState (React 19) + +**What:** React 19's `useActionState` replaces `useFormState` from react-dom/server. Works with Server Actions in Next.js App Router. + +```typescript +'use client' +import { useActionState } from 'react' +import { submitContact, type ContactState } from '@/lib/contact-action' + +const initialState: ContactState = { status: 'idle' } + +export function ContactForm() { + const [state, action, pending] = useActionState(submitContact, initialState) + + if (state.status === 'success') { + return

Thanks! Teressa will be in touch soon.

+ } + + return ( +
+ {/* Honeypot — visually hidden, bots fill it, humans ignore it */} + + + + +