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>
This commit is contained in:
583
.planning/phases/02-marketing-site/02-RESEARCH.md
Normal file
583
.planning/phases/02-marketing-site/02-RESEARCH.md
Normal file
@@ -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>
|
||||
## 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)
|
||||
Reference in New Issue
Block a user