584 lines
26 KiB
Markdown
584 lines
26 KiB
Markdown
|
|
# 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 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
|
|||
|
|
</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>© {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)
|