# 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 */}