Files
red/.planning/phases/02-marketing-site/02-RESEARCH.md
Chandler Copeland 0cdf3e5f7d 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 <noreply@anthropic.com>
2026-03-19 14:50:36 -06:00

584 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>
## 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 45 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
</user_constraints>
---
<phase_requirements>
## 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 |
</phase_requirements>
---
## 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<ContactState> {
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: `<p><strong>Name:</strong> ${data.name}</p>
<p><strong>Email:</strong> ${data.email}</p>
<p><strong>Phone:</strong> ${data.phone}</p>
<p><strong>Message:</strong><br/>${data.message.replace(/\n/g, '<br/>')}</p>`,
})
}
```
### 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<ReturnType<typeof setInterval> | 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 (
<section
id="about"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* carousel content */}
<blockquote>"{TESTIMONIALS[active].quote}"</blockquote>
<p> {TESTIMONIALS[active].name}</p>
<button onClick={prev}><ChevronLeft /></button>
<button onClick={next}><ChevronRight /></button>
<div>
{TESTIMONIALS.map((_, i) => (
<button key={i} onClick={() => setActive(i)} aria-label={`Testimonial ${i + 1}`}
style={{ opacity: i === active ? 1 : 0.3 }} />
))}
</div>
</section>
)
}
```
### 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
<a href="#hero">Home</a>
<a href="#about">About</a>
<a href="#listings">Listings</a>
<a href="#contact">Contact</a>
// Section targets
<section id="hero"> ... </section>
<section id="about"> ... </section> // testimonials anchors here per section order
<section id="listings"> ... </section>
<section id="contact"> ... </section>
```
### 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 <p>Thanks! Teressa will be in touch soon.</p>
}
return (
<form action={action}>
{/* Honeypot — visually hidden, bots fill it, humans ignore it */}
<input name="website" type="text" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<input name="name" type="text" required placeholder="Your 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?" />
{state.status === 'error' && <p role="alert">{state.message}</p>}
<button type="submit" disabled={pending}>
{pending ? 'Sending...' : 'Send Message'}
</button>
</form>
)
}
```
### Anti-Patterns to Avoid
- **Using `useFormState` from `react-dom`:** Removed in React 19. Use `useActionState` from `react` instead.
- **Storing contact form submissions in DB in this phase:** CONTEXT.md locked to email-only delivery. DB storage is out of scope.
- **Importing carousel library (Swiper, Embla):** Adds unnecessary bundle for 4-5 static testimonials. Hand-roll with useState/useEffect.
- **Putting a link to `/agent/login` on the public site:** Explicitly locked out of scope — agent navigates there directly via URL.
- **Hardcoding SMTP credentials:** Always read from `CONTACT_EMAIL_USER` / `CONTACT_EMAIL_PASS` env vars.
- **Using `router.push` for smooth scroll:** Use CSS `scroll-behavior: smooth` + `<a href="#id">` anchors instead.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| SMTP email delivery | Custom fetch to mail API | `nodemailer` | Handles STARTTLS, auth, connection pooling, error codes |
| Form validation | Manual regex/string checks | `zod` (already installed) | Already in project; handles type coercion, error messages |
| Image optimization | Raw `<img>` tags | `next/image` | Automatic WebP conversion, lazy load, responsive srcset |
| Smooth scroll | `window.scrollTo()` JS | CSS `scroll-behavior: smooth` | Native browser feature; no JS required |
**Key insight:** The SMTP connection lifecycle (STARTTLS negotiation, auth failures, timeouts, connection pooling) is non-trivial. Nodemailer handles all of it — never reimplement it.
---
## Common Pitfalls
### Pitfall 1: `useFormState` vs `useActionState` (React 19 breaking change)
**What goes wrong:** Developer uses `useFormState` from `react-dom/server` — module not found or deprecated warning.
**Why it happens:** Most tutorials pre-date React 19. The hook moved.
**How to avoid:** Import `useActionState` from `react` (not `react-dom`). React 19.2.4 is in the project.
**Warning signs:** TypeScript error "useFormState is not exported from react-dom".
### Pitfall 2: Sticky Nav Overlapping Section Anchors
**What goes wrong:** Clicking "Contact" scrolls so the section heading is hidden behind the sticky nav.
**Why it happens:** Browser positions the anchor at `top: 0`, but sticky nav covers it.
**How to avoid:** Add `scroll-margin-top` to each section equal to nav height.
```css
section[id] { scroll-margin-top: 72px; } /* match nav height */
```
### Pitfall 3: Carousel `setInterval` Memory Leak
**What goes wrong:** Multiple intervals accumulate on re-renders; carousel skips or fires multiple times per second.
**Why it happens:** `useEffect` cleanup not returning `clearInterval`.
**How to avoid:** Always clear the previous interval in the `useEffect` cleanup function. Use a `ref` to track the interval ID (shown in Pattern 3 above).
### Pitfall 4: Honeypot Field Autofilled by Browser Password Manager
**What goes wrong:** Browser autofills the honeypot field, causing legitimate submissions to be silently discarded.
**Why it happens:** The hidden field has a name that triggers autofill (e.g., `name="email2"`).
**How to avoid:** Name the honeypot something bots seek but browsers won't autofill (e.g., `name="website"` or `name="url"`). Add `autoComplete="off"` and `tabIndex={-1}`. Use `style={{ display: 'none' }}` rather than `type="hidden"` (bots fill visible-in-DOM fields more reliably).
### Pitfall 5: Nodemailer `from` Address Rejected by SMTP Server
**What goes wrong:** Email silently fails or bounces because `from` address doesn't match authenticated user.
**Why it happens:** Gmail/SMTP providers require `from` to match the authenticated account.
**How to avoid:** Set `from` to `process.env.CONTACT_EMAIL_USER` (Teressa's SMTP account) and use `replyTo` for the visitor's email. The pattern in Pattern 2 above handles this correctly.
### Pitfall 6: Next.js `Image` Component with Local File
**What goes wrong:** `<Image src="/red.jpg" />` throws "Image is missing required 'width' or 'height'" or renders distorted.
**Why it happens:** Next.js Image requires explicit dimensions OR `fill` + positioned parent.
**How to avoid:** For the hero split-panel, use `fill` with `objectFit="cover"` on a positioned container.
```tsx
// Hero photo container
<div style={{ position: 'relative', height: '100%', minHeight: '500px' }}>
<Image src="/red.jpg" alt="Teressa Copeland" fill style={{ objectFit: 'cover', objectPosition: 'center top' }} priority />
</div>
```
---
## Code Examples
### Environment Variables Required
Add to `.env.local` (never commit):
```bash
# Contact form SMTP
CONTACT_EMAIL_USER=teressa@teressacopelandhomes.com
CONTACT_EMAIL_PASS=your_app_password
CONTACT_SMTP_HOST=smtp.gmail.com
CONTACT_SMTP_PORT=587
```
Note: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` in addition to the CONTEXT.md variables — the host/port pair is required by Nodemailer but not mentioned in the user decisions. Use Gmail app password or equivalent.
### Sticky Nav with Mobile Toggle
```tsx
// app/_components/SiteNav.tsx
'use client'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'
const NAV_LINKS = [
{ label: 'Home', href: '#hero' },
{ label: 'About', href: '#about' },
{ label: 'Listings', href: '#listings' },
{ label: 'Contact', href: '#contact' },
]
export function SiteNav() {
const [open, setOpen] = useState(false)
return (
<nav style={{
position: 'sticky', top: 0, zIndex: 50,
backgroundColor: '#1B2B4B', color: '#FAF9F7',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 2rem' }}>
{/* Wordmark */}
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>Teressa Copeland</span>
{/* Desktop nav links */}
<ul className="hidden md:flex gap-8">
{NAV_LINKS.map(l => <li key={l.href}><a href={l.href}>{l.label}</a></li>)}
</ul>
{/* Mobile hamburger */}
<button className="md:hidden" onClick={() => setOpen(o => !o)}>
{open ? <X /> : <Menu />}
</button>
</div>
{/* Mobile drawer */}
{open && (
<ul style={{ backgroundColor: '#1B2B4B', padding: '1rem 2rem' }}>
{NAV_LINKS.map(l => (
<li key={l.href}><a href={l.href} onClick={() => setOpen(false)}>{l.label}</a></li>
))}
</ul>
)}
</nav>
)
}
```
### Footer with License Number
```tsx
// app/_components/SiteFooter.tsx
export function SiteFooter() {
return (
<footer style={{ backgroundColor: '#1B2B4B', color: '#FAF9F7', padding: '2rem' }}>
<p>Teressa Copeland</p>
{/* TODO: Verify license number before launch — last two chars may be "00" (zeros) or "OO" (letters O) */}
<p>Utah Real Estate License: 14196185-SA00</p>
<p>&copy; {new Date().getFullYear()} Teressa Copeland Homes. All rights reserved.</p>
</footer>
)
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `useFormState` from `react-dom` | `useActionState` from `react` | React 19 (2024) | Old import throws; use new hook |
| API Routes for form submit | Server Actions with `action={serverAction}` | Next.js 14+ | No `fetch`, no `/api/` endpoint needed |
| `pages/` router | App Router (`app/`) | Next.js 13+ | Project already uses App Router from Phase 1 |
| `<img>` tags | `next/image` | Next.js 10+ | Auto-optimization, lazy loading, responsive |
**Deprecated/outdated:**
- `useFormState` from `react-dom/server`: Removed in React 19. Use `useActionState` from `react`.
- Pages Router API routes for forms: Superseded by Server Actions in App Router.
---
## Open Questions
1. **SMTP provider / Gmail App Password**
- What we know: CONTEXT.md locked `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` as env vars
- What's unclear: Whether Teressa uses Gmail (app password), Namecheap hosting email, or another provider — affects `host`/`port` config
- Recommendation: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` env vars so the planner does not hardcode a provider. Document Gmail setup as the example (host: smtp.gmail.com, port: 587, secure: false, requires App Password with 2FA enabled).
2. **Lucide React installation status**
- What we know: CONTEXT.md says "use Lucide React (already likely available)" but it does not appear in package.json dependencies
- What's unclear: Whether it was installed but not listed, or needs to be added
- Recommendation: Plan task should `npm install lucide-react` as a safe no-op if already present.
3. **License number last two characters**
- What we know: `14196185-SA00` — user flagged uncertainty whether final chars are `00` (zeros) or `OO` (letters)
- What's unclear: The correct legal license number
- Recommendation: Add a `// TODO: VERIFY LICENSE NUMBER` comment in the footer code. Do not block implementation on this — it's a content swap before launch.
---
## Sources
### Primary (HIGH confidence)
- Next.js 16 App Router — Server Actions, `useActionState`, `next/image` patterns: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- React 19 `useActionState` API: https://react.dev/reference/react/useActionState
- Nodemailer official docs: https://nodemailer.com/about/
- Project package.json (confirmed versions): Next.js 16.2.0, React 19.2.4, Zod ^4.3.6, Tailwind ^4
### Secondary (MEDIUM confidence)
- CSS `scroll-behavior: smooth` + `scroll-margin-top` MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top
- Honeypot spam protection pattern — multiple community sources agree on `display:none` + `tabIndex={-1}` + `autoComplete="off"` approach
### Tertiary (LOW confidence — validate before implementation)
- Gmail SMTP settings (host, port, app password requirement) — widely documented but Teressa's actual provider unknown until confirmed
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — package.json confirms exact versions; no guessing
- Architecture: HIGH — Next.js App Router patterns are stable and well-documented
- Carousel pattern: HIGH — pure React useState/useEffect; no library dependency
- Server Action + Nodemailer: HIGH — Next.js official docs confirm pattern; Nodemailer is stable
- SMTP provider specifics: LOW — depends on Teressa's email provider, unknown at research time
**Research date:** 2026-03-19
**Valid until:** 2026-04-19 (30 days — Next.js 16 and Nodemailer are stable; Tailwind 4 and React 19 patterns are settling but unlikely to break)