﻿# speeddates.com — Implementation Plan

## Context

The repo currently contains the original `docs/` specs (5 .docx files) plus this plan, the [UI plan](UI_PLAN.md), a [docs viewer](viewer.html), and a [`demo/`](../demo/) folder of static HTML mockups (3 homepage directions + listings + event detail). No Next.js code yet. This plan establishes the architecture and executes **Batch 1** from [docs/05_speeddates_user_stories.docx](docs/05_speeddates_user_stories.docx) — the database schema and project foundation that every subsequent batch builds on.

**Product summary** (from docs 1, 2, 4): a worldwide, curated directory of speed-dating events. Two-sided marketplace (singles browse, organizers list). Not a ticketing platform — `Get Tickets` deep-links to the organizer's external site. Organizers go through application + admin approval; new event listings pass an automated LLM vetting check (duplicate, content moderation, embedded-URL detection) before going live.

**Confirmed decisions:**
- **Stack:** Next.js 15 (App Router, TypeScript) + **MySQL** (user already has it) + Prisma ORM + Tailwind CSS
- **Deploy target:** production server only (dev = prod for now). Auto-push on commit to `develop` is wired via `.git/hooks/post-commit`.
- **Image storage:** local filesystem under `/uploads`
- **Plan scope:** Batch 1 in execution detail; Batches 2-7 sketched

**Design:** Visual design lives in [docs/UI_PLAN.md](UI_PLAN.md) (tokens, components, page layouts) and the [`demo/`](../demo/) folder (static HTML mockups — [open `demo/index.html`](../demo/index.html) in a browser to compare directions). Tailwind config consumes the same token names defined in [`demo/styles/tokens.css`](../demo/styles/tokens.css) — keep [UI_PLAN.md](UI_PLAN.md) and the demo tokens updated whenever the design changes.

---

## Architecture Decisions

### Framework: Next.js App Router (SSR)
The PRD requires server-rendered event/city/country pages for SEO. App Router + Server Components is the natural fit. Use `force-dynamic` for listings and `revalidate` (ISR) for event/city/country detail pages so they're crawler-friendly but still cached.

### URL Routing
Mirror the PRD hierarchy directly to App Router segments:
- `/` → homepage
- `/events` → listings (all)
- `/events/[country]` → country listings
- `/events/[country]/[city]` → city listings
- `/events/[country]/[city]/[slug]` → event detail (slug = `{event-name}-{YYYY-MM-DD}`)
- `/organizers/[slug]` → public organizer profile
- `/organizers/apply` → application form (Batch 4)
- `/account/*` → attendee dashboard (Batch 3)
- `/dashboard/*` → organizer dashboard (Batch 5)
- `/admin/*` → admin panel (Batch 6)

### ORM: Prisma
Mature MySQL support, type-safe, generates migrations. Schema lives in `prisma/schema.prisma`.

### Auth: Auth.js (NextAuth v5)
Supports email/password (Credentials provider) + Google OAuth (US-014) + role-based redirects (US-030).

### Server-side deploy script
`git pull` alone is not enough for Next.js — the server must also `npm install`, `npm run build`, and restart the Node process. Create `bin/deploy.sh` in the repo so the server's post-pull hook (or our local post-commit hook) can call it. Use **PM2** to keep `next start` alive and reverse-proxy through nginx/Apache.

### LLM vetting (Batch 5)
Server-side API route calls the Anthropic API with a structured prompt (event fields + organizer's active listings). Returns JSON verdict `{pass: bool, reason: string}`. Triggered asynchronously after `Submit for Review`.

---

## Pre-Batch-1 Verification

Before any code is written, verify the dev server can host this stack. Confirm:
1. `node -v` — Next.js 15 needs Node 18.18+ (ideally 20+)
2. `npm -v`
3. `mysql --version` + access to create a database
4. PM2 available (`pm2 -v`) or willingness to install it
5. Web server (nginx/Apache) configured to proxy the public URL → `localhost:<port>`

If Node is missing, install via `nvm` (does not require root). If MySQL access requires a credential, capture the connection string for `.env`.

---

## Batch 1 — Database & Project Foundation (Detailed)

### Files to create

| Path | Purpose |
|---|---|
| `package.json` | Next.js 15, React 19, Prisma, Auth.js, Tailwind, TS, zod |
| `tsconfig.json` | TypeScript strict mode |
| `next.config.ts` | Image optimization disabled for local `/uploads`, output config |
| `tailwind.config.ts` | Theme tokens from [docs/03_speeddates_brand_design.docx](docs/03_speeddates_brand_design.docx) — burgundy `#8B1A4A`, champagne `#D4A96A`, cream `#F5ECD7`, plum `#2D1B2E`, off-white `#FDFAF5`; Inter font |
| `postcss.config.mjs` | Tailwind plugin |
| `prisma/schema.prisma` | All 6 tables (US-001..US-006) — see schema sketch below |
| `src/lib/db.ts` | Prisma client singleton (avoids hot-reload connection leaks) |
| `src/lib/config.ts` | Zod-validated env loader |
| `src/lib/slug.ts` | `slugify()` helper for country/city/event/organizer slugs (US-013 normalisation) |
| `src/app/layout.tsx` | Root layout, Inter font, brand colors |
| `src/app/page.tsx` | Placeholder homepage (Batch 2 builds it out) |
| `src/app/globals.css` | Tailwind base + brand CSS variables |
| `.env.example` | DATABASE_URL, NEXTAUTH_SECRET, ANTHROPIC_API_KEY placeholder |
| `.env` | Local secrets (git-ignored) |
| `.gitignore` | `node_modules/`, `.next/`, `.env`, `/uploads/`, `*.log` |
| `bin/deploy.sh` | Server-side: `npm ci && npx prisma migrate deploy && npm run build && pm2 reload speeddates` |
| `README.md` | Setup steps, dev/prod commands |

### Prisma schema (sketch)

Translate the 6 tables from US-001..US-006 directly. Highlights:

```prisma
enum UserRole { ATTENDEE ORGANIZER ADMIN }
enum UserStatus { ACTIVE PENDING SUSPENDED REJECTED }
enum EventStatus { DRAFT PENDING LIVE REJECTED SOLD_OUT PAST DELETED REMOVED }
enum EventType { MIXED LGBTQ_FRIENDLY WOMEN_ONLY MEN_ONLY SENIORS OTHER }

model User {
  id            Int      @id @default(autoincrement())
  email         String   @unique
  passwordHash  String?  // null until organizer approval set-password flow
  role          UserRole @default(ATTENDEE)
  status        UserStatus @default(ACTIVE)
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  organizerProfile OrganizerProfile?
  attendeeProfile  AttendeeProfile?
  events           Event[]
  savedEvents      SavedEvent[]
}

model Event {
  id              Int     @id @default(autoincrement())
  organizerId     Int
  organizer       User    @relation(fields: [organizerId], references: [id])
  title           String
  slug            String  // {slugified-title}-{YYYY-MM-DD}
  description     String  @db.Text
  venueName       String
  address         String
  city            String
  citySlug        String  // normalised, see lib/slug.ts
  country         String
  countrySlug     String  // normalised
  lat             Decimal? @db.Decimal(10, 7)
  lng             Decimal? @db.Decimal(10, 7)
  eventDate       DateTime @db.Date
  eventTime       String   // "19:30"
  ageMin          Int?
  ageMax          Int?
  eventType       EventType
  capacity        Int?
  ticketUrl       String
  status          EventStatus @default(DRAFT)
  rejectionReason String?  @db.Text
  viewCount       Int      @default(0)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  photos          EventPhoto[]
  savedBy         SavedEvent[]
  @@unique([countrySlug, citySlug, slug])
  @@index([status, eventDate])
}

// + OrganizerProfile, AttendeeProfile, EventPhoto, SavedEvent
```

(Full schema authored during execution — covers US-001 through US-006.)

### Config loader (`src/lib/config.ts`)

Zod schema validates `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET`, `ANTHROPIC_API_KEY`, `UPLOAD_DIR` at boot. Throws clear errors on startup if anything's missing.

### Slug helper (`src/lib/slug.ts`)

`slugify(string): string` — lowercase, hyphenate, strip punctuation. Also exports `normalizeCity()` and `normalizeCountry()` that apply consistent rules so e.g. `"NYC"` and `"New York"` both resolve to `new-york` (per US-013 acceptance criterion).

### Batch 1 acceptance check
- `npm run build` succeeds
- `npx prisma migrate dev --name init` applies schema cleanly
- `bin/deploy.sh` runs end-to-end on the server (manual smoke)
- `/` renders a placeholder homepage at the deploy URL

---

## Roadmap — Batches 2-7

**Batch 2 — Public browsing (no auth).** Build homepage hero + search, `/events` listing with filter bar (location, date, age group, type, distance), event detail page with photo gallery, organizer public profile. SEO foundations: hierarchical URLs, per-page `<title>`/meta, schema.org `Event` JSON-LD, sitemap.xml.

**Batch 3 — Attendee accounts.** Auth.js Credentials + Google OAuth, email verification, `/account/profile` (display name, avatar, bio), save/unsave events, `/account/saved` dashboard, password reset flow.

**Batch 4 — Organizer application & admin approval.** `/organizers/apply` form, admin panel pending-applications queue, approve/reject with email + token-gated password-set flow, organizer profile completion.

**Batch 5 — Organizer event management + LLM vetting.** `/dashboard` event CRUD, photo upload (server-side resize, ≤6 per event), draft/submit workflow, async LLM vetting (duplicate detection, content moderation, embedded-URL check) via Anthropic API, rejection reasons surfaced in dashboard, "mark sold out" toggle.

**Batch 6 — Admin panel (full).** `/admin/*` role-gated; manage organizers (suspend/reinstate), take down listings with reason, platform stats dashboard (total organizers/events/attendees/views).

**Batch 7 — Polish, perf, security, SEO.** Image pipeline (sharp): resize to 1200px, thumbnails at 600px. CSRF, rate limiting on auth endpoints, bcrypt verified, file upload validation. Dynamic sitemap + robots.txt. Mobile audit at 375/768/1280. Custom 404/500/expired-token pages. Email templates (verification, approval, rejection, password reset, takedown).

---

## Verification (Batch 1 specifically)

End-to-end smoke test after Batch 1 ships:
1. `git clone` fresh → `npm install` → `cp .env.example .env` → fill in MySQL credentials → `npx prisma migrate dev` → `npm run dev` → `http://localhost:3000` renders the placeholder homepage in brand colors.
2. Commit on `develop` → post-commit hook pushes → server pulls → `bin/deploy.sh` runs → deploy URL returns 200.
3. `npx prisma studio` shows all 6 tables created with correct columns/enums/indexes/FKs.
4. Open `http://localhost:3000` and inspect: Inter font loads, burgundy primary color visible, off-white background.

## Open items to confirm during execution

- **Pick a homepage direction** from the [demo gallery](../demo/index.html) — Batch 1 codifies tokens, but the chosen direction dictates which hero/layout we port to Next.js first
- Exact Node version on the server (may need `nvm` install)
- Whether MySQL is accessible from the Next.js Node process (same host? socket vs TCP?)
- Web server config: nginx/Apache vhost for the deploy URL → `localhost:3000` (or another port)
- PM2 install + `pm2 startup` for boot persistence
