diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d2d9abc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,482 @@ +# AGENTS.md — Coding-Agent Guide for IMPRO GENERATOR + +This file is the authoritative reference for AI coding agents working in this repository. +Read it fully before touching any file. + +--- + +## 1. Project Overview + +**IMPRO GENERATOR** is a mobile-first, PWA-ready web application that generates random +improvisation-theater prompts. Users pick a category and a difficulty level; the app draws +a random word from MongoDB while excluding already-drawn words within the same session. + +- Live URL: +- Framework: **Next.js 16.x — App Router only** (no Pages Router) +- Runtime: Node.js ≥ 18 + Vercel Edge for middleware +- Language: **TypeScript 5** — `strict: true` is enforced +- Default locale: `it` (Italian) + +--- + +## 2. Repository Layout + +``` +/ +├── src/ +│ ├── app/ +│ │ ├── [lang]/ # Every user-facing route is locale-prefixed +│ │ │ ├── (index)/ # Home — Server Component + ClientAction +│ │ │ ├── (pages)/settings/ # Settings page +│ │ │ ├── login/ # OAuth sign-in +│ │ │ ├── getDictionary.ts # Dynamic locale JSON loader +│ │ │ └── dictionaries/ # en.json it.json ro.json +│ │ ├── admin/ # Protected admin dashboard +│ │ ├── api/ +│ │ │ ├── auth/[...nextauth] # NextAuth catch-all +│ │ │ └── v1/ # All public/authed REST endpoints +│ │ ├── layout.tsx # Root HTML shell (no providers) +│ │ ├── manifest.ts # PWA Web App Manifest +│ │ └── auth.ts # NextAuth config +│ ├── components/ +│ │ ├── custom-ui/ # App-specific React components +│ │ └── ui/ # shadcn/ui primitives (do not edit by hand) +│ ├── context/ +│ │ ├── LocaleContext.tsx # Locale state + cookie + URL sync +│ │ └── ThemeContext.tsx # Theme state + localStorage +│ ├── hooks/ +│ │ ├── useLongPress.ts # Tap vs. 600 ms long-press +│ │ ├── useDoubleClick.ts # Double-click detection +│ │ └── useOfflineWordCache.ts # Client-side word cache for offline mode +│ ├── lib/ +│ │ ├── general.ts # Shared utility functions +│ │ ├── rateLimit.ts # In-memory per-IP rate limiter (60 req/min) +│ │ ├── isAdmin.ts # Admin-check helper (DB flag + env fallback) +│ │ ├── offlineWordCache.ts # Offline cache logic +│ │ ├── utils.ts # cn() and other shadcn utilities +│ │ └── db/ +│ │ ├── mongodb.ts # Mongoose connection singleton +│ │ ├── mongodbClient.ts # Raw MongoClient for NextAuth adapter +│ │ ├── models/ # Mongoose schemas (Category, Word, User, …) +│ │ ├── queries/ # Server-side cached query functions +│ │ ├── seed/ # seed.ts, seedFromCsv.ts +│ │ └── types/ # TypeScript interfaces mirroring DB models +│ ├── proxy.ts # Middleware: locale negotiation + auth re-export +│ └── types/ # Global type augmentations (next-auth session) +├── assets/ # Static seed assets (word-list CSVs, etc.) +├── public/ # Static files served at / +├── next.config.ts # Next.js + PWA (Workbox) config +├── tailwind.config.ts +├── tsconfig.json # strict: true, paths alias @/* → src/* +└── eslint.config.mjs +``` + +--- + +## 3. Development Commands + +```bash +# Install dependencies +npm install + +# Start dev server (Turbopack, http://localhost:3000) +npm run dev + +# Production build (also generates the service worker) +npm run build + +# Start production server +npm run start + +# Lint (eslint-config-next + core-web-vitals + TypeScript rules) +npm run lint + +# Seed structured data (categories, languages, words) into MongoDB +npm run seed + +# Seed additional words from CSV files in /assets/wordlist/ +npm run seed:csv +``` + +There is **no test runner** configured in this repository. Validate changes by running +`npm run build` (catches TypeScript errors) and `npm run lint`. + +--- + +## 4. Environment Variables + +Create `.env.local` in the project root. **Never commit this file.** + +```env +# ── Required ───────────────────────────────────────────────────────────────── +# src/lib/db/mongodb.ts reads MONGO_URI (not MONGODB_URI). +MONGO_URI=mongodb+srv://:@.mongodb.net/?retryWrites=true&w=majority + +# ── NextAuth v5 ─────────────────────────────────────────────────────────────── +AUTH_SECRET= +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= + +# ── Admin bootstrap (optional) ──────────────────────────────────────────────── +# Comma-separated email list — grants admin before any DB record has isAdmin=true +ADMIN_EMAILS=you@example.com +``` + +All runtime secret reads go through `process.env.*`; never hard-code credentials. + +--- + +## 5. Next.js Architecture Rules + +### 5.1 App Router only + +All routes live under `src/app/`. The Pages Router (`src/pages/`) does **not** exist and +must not be created. + +### 5.2 Locale-prefixed routes + +Every user-facing page **must** live under `src/app/[lang]/`. The `[lang]` dynamic segment +is the locale code (`en`, `it`, `ro`). The middleware (`src/proxy.ts`) redirects bare paths +to the correct locale automatically. + +Do not hardcode locale prefixes outside of `src/proxy.ts` and `src/app/[lang]/getDictionary.ts`. + +### 5.3 Server Components vs. Client Components + +| Rule | Detail | +|------|--------| +| Default to **Server Component** | Omit `"use client"` unless the component needs browser APIs, state, or event handlers. | +| Add `"use client"` at the **top of the file** | It must be the very first line, before any imports. | +| Never call `auth()` from a Client Component | Use `useSession()` from `next-auth/react` instead. | +| Never import a Client Component into a Server Component **without a boundary** | Wrap in a Client parent or pass as `children`. | +| Server Components can be `async` | Call `await connectDB()` and Mongoose queries directly. | + +Current Client Components: `ClientAction`, `ActionButton`, `Screen`, `LevelChecker`, +`StopWatch`, `Navbar`, `ToggleAction`, `LocaleContext`, `ThemeContext`, `AdminDashboard`. + +### 5.4 Route Handlers (API) + +- Location: `src/app/api/v1//route.ts` +- Export named HTTP-method functions: `export async function GET(req: NextRequest) {}` +- Always call `rateLimit(getClientIp(req))` at the top of every public endpoint and + return `429` with a `Retry-After` header when the limit is exceeded. +- Use `auth()` from `@/app/auth` for protected endpoints. +- Set `Cache-Control` headers explicitly: + - Deterministic list responses: `public, max-age=3600, stale-while-revalidate=86400` + - Random / stochastic responses: `no-store` + - Mutations: `no-store` + +### 5.5 Middleware + +`src/middleware.ts` re-exports `auth` as the default export from NextAuth — this is how +Next.js picks up the auth session on every request. The locale-negotiation function +(`proxy()`) in `src/proxy.ts` is imported by middleware but runs **inside** auth's +callback: + +``` +Request → NextAuth middleware (src/proxy.ts re-export) → locale negotiation → handler +``` + +The middleware matcher excludes `api`, `admin`, `_next/static`, `_next/image`, and static +assets — do not add new exclusions without updating the matcher in `src/proxy.ts`. + +### 5.6 ISR and Data Caching + +Use `unstable_cache` from `next/cache` for server-side data that should survive across +requests: + +```ts +export const getCategories = unstable_cache(fetchFn, ['categories'], { + revalidate: 3600, + tags: ['categories'], +}); +``` + +Call `revalidateTag('categories')` from a Route Handler or Server Action after any +mutation that changes category data. Do not add `export const revalidate = ...` to a +page if the data already has its own `unstable_cache` TTL — they would conflict. + +The home page (`src/app/[lang]/(index)/page.tsx`) sets `export const revalidate = 3600` +as a page-level ISR guard. + +--- + +## 6. TypeScript Conventions + +- **`strict: true`** — all types must be explicit; avoid `any` except where the ESLint + rule is intentionally disabled (`// NO explicit any IS ONLY FOR DEMO PURPOSES`). +- Path alias: `@/*` maps to `src/*`. Always use `@/` imports, never relative `../../`. +- Interfaces for DB types live in `src/lib/db/types/`. Match them to the Mongoose schema. +- Augment NextAuth session type in `src/types/` (e.g., adding `isAdmin` to `session.user`). +- All React component props must be typed (no implicit `any` props). + +--- + +## 7. Database Patterns + +### 7.1 Connection singleton + +Always use `connectDB()` from `src/lib/db/mongodb.ts` before every Mongoose query. +It caches the connection on the global object to survive hot-reloads in development. + +```ts +await connectDB(); +const words = await Word.find(query); +``` + +Never instantiate a new `mongoose.connect()` call directly. + +### 7.2 Model registration guard + +Every model file uses the singleton pattern: + +```ts +const Word: Model = + mongoose.models.Word || mongoose.model('Word', WordSchema); +``` + +Always follow this pattern. Do not call `mongoose.model()` unconditionally — it throws +on hot reload. + +### 7.3 Multilingual fields + +`Word.word` and `Category.name` / `Category.description` are Mongoose `Map` fields +keyed by locale code (`en`, `it`, `ro`). When reading them in a component or API: + +```ts +const display = word.word[locale] ?? word.word['en'] ?? ''; +``` + +Always fall back to `'en'` if the requested locale is absent. + +### 7.4 Difficulty enum + +```ts +type Difficulty = 'easy' | 'medium' | 'hard'; +``` + +The API accepts numeric aliases (`1`=easy, `2`=medium, `3`=hard) for backwards +compatibility, but the DB stores the string form. Map via `LEVEL_ALIAS` in +`src/app/api/v1/words/route.ts`. + +### 7.5 Aggregation over N+1 + +Prefer MongoDB aggregations (`$lookup`, `$group`, `$sample`) over multiple sequential +queries. Example: counting words per category is done in a single `$lookup` pipeline in +`getCategories.ts` — do not replace this with `Word.countDocuments()` in a loop. + +--- + +## 8. API Conventions + +### 8.1 Rate limiting + +Every public `GET` endpoint must start with: + +```ts +const { ok, retryAfter } = rateLimit(getClientIp(req)); +if (!ok) { + return NextResponse.json( + { error: 'Too many requests. Please try again later.' }, + { status: 429, headers: { 'Retry-After': String(retryAfter) } } + ); +} +``` + +Current window: 60 requests per IP per 60 seconds (configured in `src/lib/rateLimit.ts`). + +### 8.2 Auth guard for mutations + +```ts +const session = await auth(); +if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +} +``` + +Admin-only endpoints additionally call `isAdmin(session)` from `src/lib/isAdmin.ts`. + +### 8.3 Response shape + +| Scenario | Shape | +|----------|-------| +| List endpoint | `{ metadata: { total, … }, data: T[] }` | +| Random draw | `{ data: T[] }` (array with 0 or 1 item) | +| Single resource | the resource object directly | +| Error | `{ error: string }` | + +### 8.4 Versioning + +All new endpoints go under `/api/v1/`. Do not create `/api/v0/` routes — the v0 prefix +is reserved for the legacy CSV-based API and is deprecated. + +--- + +## 9. Internationalization Conventions + +### 9.1 Supported locales + +`it` (default), `en`, `ro`. Defined in two places — keep them in sync: + +- `src/proxy.ts` — `const locales = ['it', 'ro', 'en']` +- `src/app/[lang]/getDictionary.ts` — the `languages` array + +### 9.2 UI strings + +Load via `getDictionary(locale)` in Server Components (it returns a JSON object). +In Client Components use `useLocale()` from `LocaleContext` to get the `dictionary`. + +Never hard-code user-visible strings in English — always add the key to all three +dictionary files (`en.json`, `it.json`, `ro.json`). + +### 9.3 Locale cookie + +The active locale is persisted in a cookie named `locale` with a 1-year max-age. +The cookie is set by the middleware on every request and by `handleSetLocale` in +`LocaleContext` on user selection. + +### 9.4 Adding a new locale + +1. Add a `{lang}.json` dictionary file in `src/app/[lang]/dictionaries/`. +2. Append the locale code to `locales` in `src/proxy.ts`. +3. Append the locale to the `languages` array in `src/app/[lang]/getDictionary.ts`. +4. Add a `{ lang: '' }` entry to `generateStaticParams` in + `src/app/[lang]/(index)/page.tsx`. +5. Seed a new `Language` document and translated words via `npm run seed`. + +--- + +## 10. Authentication Patterns + +### 10.1 NextAuth v5 (beta) + +Config: `src/app/auth.ts`. Exports: `{ handlers, signIn, signOut, auth }`. + +- Session strategy: **JWT** (no DB session table). +- The `jwt` callback reads `isAdmin` from the `User` collection and attaches it to the + token on every sign-in. +- The `session` callback forwards `isAdmin` onto `session.user`. + +### 10.2 Checking admin access + +```ts +// Server Component or Route Handler +import { auth } from '@/app/auth'; +import { isAdmin } from '@/lib/isAdmin'; + +const session = await auth(); +if (!isAdmin(session)) redirect('/'); +``` + +`isAdmin()` returns `true` if `session.user.isAdmin === true` **or** if the user's email +is in the `ADMIN_EMAILS` environment variable (comma-separated). The env var is the +bootstrap escape hatch before the first admin DB record is created. + +### 10.3 Client-side auth + +```ts +import { useSession } from 'next-auth/react'; +const { data: session } = useSession(); +``` + +Never call `auth()` in a Client Component — it is a Server-only function. + +### 10.4 Sign-in / error pages + +Both are routed to `/it/login` (hardcoded in `src/app/auth.ts`). + +--- + +## 11. PWA & Service Worker + +The service worker is generated by `@ducanh2912/next-pwa` (Workbox) during +`npm run build`. It is **not** active in development. + +Caching strategy (configured in `next.config.ts`): + +| Resource pattern | Strategy | TTL | +|------------------|----------|-----| +| `/_next/static/**` | CacheFirst | 30 days | +| `/_next/image?*` | CacheFirst | 7 days | +| `/api/v1/categories*` | StaleWhileRevalidate | 24 h | +| `/api/v1/words*` | NetworkFirst (10 s) | 24 h | +| HTML pages (prod domain) | NetworkFirst (10 s) | 24 h | + +Do not change these strategies without understanding how they interact with the +`Cache-Control` headers set in the Route Handlers. + +--- + +## 12. Styling Conventions + +- **Tailwind CSS v3** only — no inline `style={}` props unless animating dynamic values. +- Use `cn()` from `src/lib/utils.ts` (re-export of `clsx` + `tailwind-merge`) to merge + conditional classes. +- Dark-mode is class-based: `darkMode: ["class"]` in `tailwind.config.ts`. The root + `` element gets `class="dark"` via `next-themes`. +- Custom animations (`marquee`, `marquee2`) and the monospace font stack (`Geist Mono`) + are declared in `tailwind.config.ts` — extend there, not in `globals.css`. +- The retro CRT / Nokia aesthetic lives in `src/app/[lang]/globals.css`. Keep all + global style overrides there. +- **Never edit** files under `src/components/ui/` directly — they are managed by + `shadcn/ui` and will be overwritten on upgrades. + +--- + +## 13. Component Conventions + +- One component per file, file name matches the component name (`PascalCase.tsx`). +- Custom app components go in `src/components/custom-ui/`. +- Shared logic extracted to `src/hooks/` as `useCamelCase.ts` hooks. +- Context providers live in `src/context/` and export both the provider and a + `use()` hook that throws if called outside the provider. + +--- + +## 14. How to Verify Changes + +Since there is no test suite, verify changes as follows: + +1. **Type-check + build** + ```bash + npm run build + ``` + A successful build means no TypeScript errors and no broken imports. + +2. **Lint** + ```bash + npm run lint + ``` + Fix all errors; warnings from disabled rules (`no-explicit-any`, + `no-unescaped-entities`) can be ignored if intentional. + +3. **Local dev smoke-test** + ```bash + npm run dev + ``` + - Visit `http://localhost:3000` → should redirect to `http://localhost:3000/it`. + - Test the main draw flow: select a category, tap a level, verify a word appears. + - Switch locale via settings and verify the URL changes. + +4. **API smoke-test** (requires a running MongoDB) + ``` + GET http://localhost:3000/api/v1/categories + GET http://localhost:3000/api/v1/words?action=&level=easy&sample=1 + ``` + +--- + +## 15. Common Pitfalls to Avoid + +| Pitfall | Correct approach | +|---------|-----------------| +| Calling `auth()` in a Client Component | Use `useSession()` from `next-auth/react` | +| Importing from `@/app/auth` in a Client Component | Move auth logic to a Server Component or Route Handler | +| Creating a new Mongoose model with `mongoose.model(...)` unconditionally | Always use the `mongoose.models.X \|\| mongoose.model(...)` guard | +| Hardcoding locale strings (`/en/`, `/it/`) outside proxy/getDictionary | Read from the `locales` array in `src/proxy.ts` | +| Adding `Cache-Control: no-store` to a list endpoint | Only use `no-store` for random/stochastic responses | +| Skipping `rateLimit()` in a new Route Handler | Every public endpoint needs the rate-limit guard | +| Editing `src/components/ui/` files | These are shadcn-managed; customise via Tailwind tokens instead | +| Adding a new locale without updating both `src/proxy.ts` and `getDictionary.ts` | Both arrays must stay in sync or middleware will loop | diff --git a/README.md b/README.md index 7e4fad9..2224b84 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # 🎭 IMPRO GENERATOR -> A mobile-first random prompt generator for improvisation theater. +> A mobile-first random prompt generator for improvisation theater — built with the Next.js App Router, React 19, and MongoDB. + This project was created for fun, with the idea of having a tool for improv shows or lessons. This web app (and potentially a future mobile app) aims to be creative and enjoyable. It is a friendly repository for anyone who wants to contribute and have fun. [![Deployed on Vercel](https://img.shields.io/badge/deployed-vercel-black?logo=vercel)](https://impro-generator.vercel.app) -[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org) +[![Next.js](https://img.shields.io/badge/Next.js-16.x-black?logo=next.js)](https://nextjs.org) +[![React](https://img.shields.io/badge/React-19-61DAFB?logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript)](https://www.typescriptlang.org) [![License](https://img.shields.io/badge/license-private-lightgrey)](#) Pick a **category** and a **difficulty level** — get a random word to build your improv scene around. Tracks already-drawn words so you never repeat until the pool is exhausted. Designed to be used live on stage from a phone or tablet. @@ -14,31 +17,177 @@ Pick a **category** and a **difficulty level** — get a random word to build yo ## ✨ Features - 🎲 **Random word draw** per category and difficulty (Easy / Medium / Hard) -- 🔁 **Anti-repeat sampling** — drawn words are excluded until the pool resets -- ⏱ **Configurable stopwatch** with WakeLock (screen stays on while timing) -- 🌍 **Multi-language** — English, Italian, Romanian (UI + words) -- 📱 **Mobile-first** with haptic feedback -- 🌙 **Dark / Light / System** theme -- 🔁 **Not repeating words** the routes have a call specific logic to not repeat the calls on words +- 🔁 **Anti-repeat sampling** — drawn words are excluded client-side until the pool resets +- ⏱ **Configurable stopwatch** with WakeLock API (screen stays on while timing) +- 🌍 **Multi-language** — English, Italian, Romanian (UI strings + database words) +- 📱 **Mobile-first** with haptic feedback via the Vibration API +- 🌙 **Dark / Light / System** theme with `next-themes` - 📟 Retro Nokia/CRT visual aesthetic -- 🗃 Long-press any category button to browse the full word list +- 🗃 Long-press any category button to browse the full word list in a drawer +- 🔐 Authentication via GitHub & Google OAuth (NextAuth v5) +- 🛡 Rate-limited API endpoints (in-memory, per-IP) +- 📦 **PWA-ready** — installable, works offline with a multi-tier service-worker cache --- ## 🛠 Tech Stack -| Layer | Technology | -| ---------- | ------------------------------------------------------------------------- | -| Framework | [Next.js](https://nextjs.org) 16 (App Router) | -| UI | React 19 +[shadcn/ui](https://ui.shadcn.com) + [Radix UI](https://radix-ui.com) | -| Styling | Tailwind CSS 3 | -| Database | MongoDB via[Mongoose](https://mongoosejs.com) | -| Table | [@tanstack/react-table](https://tanstack.com/table) v8 | -| Carousel | [Embla Carousel](https://www.embla-carousel.com) | -| Toasts | [Sonner](https://sonner.emilkowal.ski) | -| Haptics | [Tactus](https://github.com/nicktindall/tactus) | -| Analytics | [Vercel Analytics](https://vercel.com/analytics) | -| Deployment | [Vercel](https://vercel.com) | +| Layer | Technology | +| -------------- | --------------------------------------------------------------------------------------- | +| Framework | [Next.js](https://nextjs.org) 16.x — **App Router**, Turbopack (dev) | +| Language | TypeScript 5 | +| UI | React 19 + [shadcn/ui](https://ui.shadcn.com) + [Radix UI](https://radix-ui.com) | +| Styling | Tailwind CSS v3 | +| Database | MongoDB 6 via [Mongoose](https://mongoosejs.com) 8 | +| Auth | [NextAuth v5](https://authjs.dev) (beta) — GitHub & Google providers, JWT sessions | +| Table | [@tanstack/react-table](https://tanstack.com/table) v8 | +| Carousel | [Embla Carousel](https://www.embla-carousel.com) | +| Toasts | [Sonner](https://sonner.emilkowal.ski) | +| Haptics | [Tactus](https://github.com/nicktindall/tactus) | +| PWA | [@ducanh2912/next-pwa](https://ducanh-next-pwa.vercel.app) + Workbox runtime caching | +| Analytics | [Vercel Analytics](https://vercel.com/analytics) + Speed Insights | +| Deployment | [Vercel](https://vercel.com) | + +--- + +## 🏗 Next.js Architecture + +### App Router & Route Conventions + +Every user-facing route lives under `src/app/[lang]/`, making the locale a required URL segment. This approach avoids subdomain complexity and keeps locale state in the URL for easy sharing and caching. + +``` +/en → src/app/[lang]/page.tsx (Server Component, ISR) +/en/settings → src/app/[lang]/(pages)/settings (Client-heavy settings page) +/admin → src/app/admin/page.tsx (protected, server-rendered) +``` + +### Server Components vs. Client Components + +The app follows the **"push state down"** pattern — the outermost layer is a Server Component and only the interactive leaves are Client Components: + +| Component | Type | Reason | +|-----------|------|--------| +| `app/[lang]/page.tsx` | **Server** | Fetches categories from DB at render time (ISR) | +| `ClientAction.tsx` | **Client** (`"use client"`) | Manages draw state, exclude list, and haptics | +| `ActionButton.tsx` | **Client** | Long-press gesture detection | +| `StopWatch.tsx` | **Client** | `setInterval`, WakeLock API | +| `Screen.tsx` | **Client** | Animated CRT display | +| `Navbar.tsx` | **Client** | Active-route highlighting | + +### Route Handlers (API) + +All API routes are under `src/app/api/v1/` and export named HTTP-method functions — Next.js 13+ convention for [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers): + +```ts +// src/app/api/v1/words/route.ts +export async function GET(req: NextRequest) { … } +``` + +Random-draw responses include `Cache-Control: no-store` to prevent caching of stochastic results; list responses use `public, max-age=3600, stale-while-revalidate=86400`. + +### Middleware & Locale Negotiation + +`src/proxy.ts` exports a `proxy()` function and re-exports `auth` from NextAuth as the actual `middleware` default export. The locale negotiation logic runs before auth: + +**Priority order:** +1. URL path prefix (`/en/…`, `/it/…`, `/ro/…`) +2. `locale` cookie (persisted across sessions) +3. `Accept-Language` header (negotiated via `@formatjs/intl-localematcher` + `negotiator`) +4. Default: `it` + +The middleware also sets/refreshes the `locale` cookie on every response. + +### ISR & Data Caching + +Category data is cached using Next.js [`unstable_cache`](https://nextjs.org/docs/app/api-reference/functions/unstable_cache) with a 1-hour TTL and a `'categories'` tag for on-demand revalidation: + +```ts +// src/lib/db/queries/getCategories.ts +export const getCategories = unstable_cache(fetchCategories, ['categories'], { + revalidate: 3600, + tags: ['categories'], +}); +``` + +To purge the cache after a DB mutation call `revalidateTag('categories')` from a Server Action or Route Handler. + +--- + +## 🔐 Authentication + +Powered by **NextAuth v5** (`next-auth@beta`) with the [MongoDB Adapter](https://authjs.dev/reference/adapter/mongodb). + +| Provider | Flow | +|----------|------| +| GitHub | OAuth 2.0 — callback: `/api/auth/callback/github` | +| Google | OAuth 2.0 — callback: `/api/auth/callback/google` | + +Sessions use **JWT strategy** (no DB session table). The `jwt` callback enriches the token with an `isAdmin` flag read from the `User` collection on each sign-in. + +```ts +// src/app/auth.ts (simplified) +export const { handlers, signIn, signOut, auth } = NextAuth({ + adapter: MongoDBAdapter(clientPromise), + session: { strategy: "jwt" }, + providers: [GitHub(…), Google(…)], + callbacks: { + async jwt({ token, user }) { + if (user?.email) { + const dbUser = await User.findOne({ email: token.email }).lean(); + token.isAdmin = dbUser?.isAdmin ?? false; + } + return token; + }, + }, +}); +``` + +Sign-in page: `/it/login` +Admin guard: check `session.user.isAdmin` in Server Components or Route Handlers. + +### Required Auth Environment Variables + +```env +AUTH_SECRET= # Required by NextAuth v5 +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= +``` + +--- + +## 📦 PWA & Service Worker Caching + +Configured via `@ducanh2912/next-pwa` (Workbox under the hood) in `next.config.ts`. The service worker applies a **tiered caching strategy**: + +| Resource | Strategy | TTL | +|----------|----------|-----| +| `/_next/static/**` | CacheFirst | 30 days | +| `/_next/image?*` | CacheFirst | 7 days | +| `/api/v1/categories` | StaleWhileRevalidate | 24 h | +| `/api/v1/words` | NetworkFirst (10 s timeout) | 24 h | +| HTML pages | NetworkFirst (10 s timeout) | 24 h | + +**Turbopack** is enabled for local development (`next dev --turbopack` is the default via `turbopack: {}` in `next.config.ts`). The service worker is only generated in production builds. + +--- + +## 🌍 Internationalization + +Routes are prefixed by locale: `/en/`, `/it/`, `/ro/`. + +- Active locale is stored in a **cookie** (`locale=`) and read by both the middleware and the `LocaleContext` client provider +- UI strings live in `src/app/[lang]/dictionaries/{lang}.json` and are loaded with dynamic `import()` — zero bundle overhead for unused locales +- Words in the database are stored **multilingually** as a Mongoose `Map` — the display layer reads `word[locale] ?? word.en` + +**To add a new language:** + +1. Add a `{lang}.json` dictionary file in `src/app/[lang]/dictionaries/` +2. Add the locale code to the `locales` array in `src/proxy.ts` and the `languages` array in `src/app/[lang]/getDictionary.ts` +3. Add a `generateStaticParams` entry in `src/app/[lang]/layout.tsx` +4. Seed the language record and translated words via `npm run seed` --- @@ -46,8 +195,8 @@ Pick a **category** and a **difficulty level** — get a random word to build yo ### Prerequisites -- Node.js ≥ 18 -- A MongoDB instance (local or [MongoDB Atlas](https://www.mongodb.com/atlas)) +- **Node.js ≥ 18** (LTS recommended) +- A **MongoDB** instance — local (`mongod`) or [MongoDB Atlas](https://www.mongodb.com/atlas) free tier ### Installation @@ -59,12 +208,22 @@ npm install ### Environment Variables -Create a `.env.local` file in the project root: +Create a `.env.local` file in the project root. Only `MONGODB_URI` is required to run the app locally; OAuth variables are needed for authentication features. ```env +# ── Database ───────────────────────────────────────────── MONGODB_URI=mongodb+srv://:@.mongodb.net/?retryWrites=true&w=majority + +# ── NextAuth v5 ────────────────────────────────────────── +AUTH_SECRET= +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= ``` +> **Tip:** Generate `AUTH_SECRET` with `openssl rand -base64 32` or `npx auth secret`. + ### Database Seeding ```bash @@ -75,13 +234,26 @@ npm run seed npm run seed:csv ``` -### Run Locally +### Development ```bash -npm run dev +npm run dev # Starts Next.js dev server with Turbopack on http://localhost:3000 ``` -Open [http://localhost:3000](http://localhost:3000) — you will be redirected to `/en` (or your browser's preferred language). +Visiting `http://localhost:3000` redirects to `/{locale}` based on your `Accept-Language` header or the `locale` cookie. + +### Production Build + +```bash +npm run build # Type-checks, compiles, generates service worker +npm run start # Starts the production server +``` + +### Linting + +```bash +npm run lint # ESLint via eslint-config-next +``` --- @@ -90,117 +262,143 @@ Open [http://localhost:3000](http://localhost:3000) — you will be redirected t ``` src/ ├── app/ -│ ├── [lang]/ # All routes are locale-prefixed (/en, /it, /ro) -│ │ ├── page.tsx # Home — server component, fetches categories -│ │ ├── layout.tsx # Root layout with theme + locale providers -│ │ ├── globals.css # Global styles + CRT/Nokia effects -│ │ ├── getDictionary.ts # Loads the locale JSON dictionary -│ │ ├── dictionaries/ # en.json, it.json, ro.json -│ │ └── (pages)/ -│ │ └── settings/ # Language, theme, stopwatch settings -│ └── api/ -│ ├── v0/action/ # Legacy CSV-based API (deprecated) -│ └── v1/ -│ ├── categories/ # GET all categories with word counts -│ ├── languages/ # GET all supported languages -│ └── words/ # GET words with filtering and random sampling +│ ├── [lang]/ # Locale-prefixed routes (/en, /it, /ro) +│ │ ├── (index)/ # Home route group +│ │ │ ├── page.tsx # Server Component — fetches categories (ISR) +│ │ │ ├── ClientAction.tsx # "use client" root game-state manager +│ │ │ └── SuggestDialog.tsx # Word suggestion dialog +│ │ ├── (pages)/ +│ │ │ └── settings/ # Language, theme, stopwatch settings +│ │ ├── login/ # OAuth sign-in page +│ │ ├── layout.tsx # Root layout — ThemeProvider + LocaleProvider +│ │ ├── globals.css # Global styles + CRT/Nokia effects +│ │ └── getDictionary.ts # Dynamic locale JSON loader +│ │ └── dictionaries/ # en.json it.json ro.json +│ ├── admin/ # Protected admin dashboard (isAdmin guard) +│ ├── api/ +│ │ ├── auth/[...nextauth]/ # NextAuth catch-all handler +│ │ └── v1/ +│ │ ├── categories/route.ts # GET — categories with word counts +│ │ ├── languages/route.ts # GET — supported languages +│ │ ├── words/route.ts # GET — filtered + sampled words (rate-limited) +│ │ ├── words/[id]/route.ts # GET | PATCH | DELETE — single word +│ │ ├── history/route.ts # GET | POST — draw history +│ │ ├── likes/route.ts # POST — like/unlike a word +│ │ └── suggestions/route.ts # POST — submit a word suggestion +│ ├── layout.tsx # Root HTML shell (no providers) +│ ├── manifest.ts # next/dist MetadataRoute.Manifest (PWA) +│ └── auth.ts # NextAuth config (providers, callbacks) ├── components/ -│ ├── custom-ui/ # App-specific components -│ │ ├── ClientAction.tsx # Root game state manager -│ │ ├── ActionButton.tsx # Category button (tap = draw, long-press = browse) -│ │ ├── Screen.tsx # CRT display area -│ │ ├── LevelChecker.tsx # Difficulty selector -│ │ ├── StopWatch.tsx # Scene timer with WakeLock -│ │ ├── Navbar.tsx # Bottom navigation -│ │ └── ToggleAction.tsx # Simple action button variant -│ └── ui/ # shadcn/ui components +│ ├── custom-ui/ # App-specific components +│ │ ├── ActionButton.tsx # Category button — tap draws, long-press browses +│ │ ├── Screen.tsx # CRT display area +│ │ ├── LevelChecker.tsx # Difficulty selector (Easy/Medium/Hard) +│ │ ├── StopWatch.tsx # Scene timer with WakeLock +│ │ ├── Navbar.tsx # Bottom navigation bar +│ │ └── ToggleAction.tsx # Simple toggle button variant +│ └── ui/ # shadcn/ui primitives ├── context/ -│ ├── LocaleContext.tsx # Locale state + cookie persistence -│ └── ThemeContext.tsx # Theme state + localStorage persistence +│ ├── LocaleContext.tsx # Locale state + cookie persistence +│ └── ThemeContext.tsx # Theme state + localStorage persistence ├── hooks/ -│ └── useLongPress.ts # Distinguishes tap vs. 600ms long-press -└── lib/ - ├── general.ts # Shared utilities - └── db/ - ├── mongodb.ts # Mongoose connection singleton - ├── models/ # Category, Language, Word schemas - ├── queries/ # getCategories (with ISR cache) - ├── seed/ # seed.ts, seedFromCsv.ts - └── types/ # TypeScript types mirroring DB models +│ └── useLongPress.ts # Tap vs. 600 ms long-press detection +├── lib/ +│ ├── general.ts # Shared utility functions +│ ├── rateLimit.ts # In-memory per-IP rate limiter +│ └── db/ +│ ├── mongodb.ts # Mongoose connection singleton (cached) +│ ├── mongodbClient.ts # Raw MongoClient for NextAuth adapter +│ ├── models/ # Category, Language, Word, User schemas +│ ├── queries/ +│ │ └── getCategories.ts # unstable_cache wrapper — 1 h ISR +│ ├── seed/ +│ │ ├── seed.ts # Structured seed (categories + words) +│ │ └── seedFromCsv.ts # CSV bulk import +│ └── types/ # TypeScript interfaces mirroring DB models +├── proxy.ts # Middleware: locale negotiation + auth export +└── types/ # Global type augmentations (next-auth, etc.) ``` --- ## 🔌 API Reference +All endpoints are under `/api/v1/`. Random-draw responses are never cached (`Cache-Control: no-store`); list responses use `max-age=3600, stale-while-revalidate=86400`. + ### `GET /api/v1/categories` -Returns all categories with a pre-computed `wordCount`. +Returns all categories with a pre-computed `wordCount` (single `$lookup` aggregation — no N+1). ```json [ - { "_id": "...", "name": { "en": "Place", "it": "Luogo" }, "wordCount": 42 } + { "_id": "663f…", "name": { "en": "Place", "it": "Luogo", "ro": "Loc" }, "wordCount": 42 } ] ``` ### `GET /api/v1/words` -| Parameter | Type | Description | -| ----------- | ----------------------- | ------------------------------------------------- | -| `action` | string | Category ObjectId to filter by | -| `level` | `1` \| `2` \| `3` | Difficulty (1 = Easy, 2 = Medium, 3 = Hard) | -| `limit` | number | Max results (1–200, default 1) | -| `sample` | `1` | Use MongoDB `$sample` for a random draw | -| `exclude` | string | Comma-separated Word IDs to exclude from sampling | +| Parameter | Type | Description | +|-----------|------|-------------| +| `action` | `string` | Category `ObjectId` to filter by (`"all"` = no filter) | +| `level` | `easy` \| `medium` \| `hard` \| `1` \| `2` \| `3` | Difficulty filter | +| `limit` | `number` | Max results (1–200). Omit for all. | +| `sample` | `1` | Use MongoDB `$sample` for a true random draw | +| `exclude` | `string` | Comma-separated `Word` IDs to exclude from sampling | + +**Sample draw** (`sample=1`) returns `{ data: [word] }` and bypasses the list cache. +**List draw** returns `{ metadata: { total, action, level, limit }, data: […words] }`. + +Rate limit: **30 requests / 60 s** per IP. Returns `429 Too Many Requests` with `Retry-After` header on breach. ### `GET /api/v1/languages` -Returns all supported language documents. +Returns all language documents from the DB. ---- +### `GET /api/v1/words/:id` · `PATCH /api/v1/words/:id` · `DELETE /api/v1/words/:id` -## 🌍 Internationalization +Single-word CRUD — `PATCH` and `DELETE` require an authenticated admin session. -Routes are prefixed by locale: `/en/`, `/it/`, `/ro/`. +### `POST /api/v1/suggestions` -- Active locale is stored in a **cookie** (`locale=`) -- UI strings live in `src/app/[lang]/dictionaries/{lang}.json` -- Words in the database are stored **multilingually** as a `Map` — the display layer reads `word[locale] ?? word.en` -- Language negotiation on first visit uses `@formatjs/intl-localematcher` + `negotiator` +Submit a user word suggestion. Stored for admin review. -To add a new language: +### `POST /api/v1/likes` -1. Add a `{lang}.json` dictionary file in `src/app/[lang]/dictionaries/` -2. Register the locale in `src/app/[lang]/layout.tsx` static params -3. Seed the language record and translated words via `npm run seed` +Toggle a like on a word for the authenticated user. --- ## 🚢 Deployment -Deployed on [Vercel](https://vercel.com). Required environment variable in Vercel project settings: +Deployed on **Vercel** (zero-config for Next.js). Set the following environment variables in your Vercel project settings: ``` -MONGODB_URI= +MONGODB_URI +AUTH_SECRET +AUTH_GITHUB_ID +AUTH_GITHUB_SECRET +AUTH_GOOGLE_ID +AUTH_GOOGLE_SECRET ``` -The home page uses ISR with a 1-hour revalidation window (`revalidate = 3600`). Categories can be revalidated on-demand using the `'categories'` cache tag. +**ISR on Vercel:** The home page revalidates categories every 3 600 s. You can trigger an on-demand revalidation by calling `revalidateTag('categories')` from an admin Route Handler or Server Action. + +**Service worker on Vercel:** The PWA service worker is generated at build time by `@ducanh2912/next-pwa` and deployed to `/public`. No extra Vercel configuration is required. --- ## 🗺 Roadmap - [ ] Complete FAB button actions -- [ ] Scene Card Mode — generate a full scene prompt in one tap Character + Location + Situation + Relation all at once, shown as a card -- [ ] PWA / offline support with pre-cached word lists - This is used live on stage where Wi-Fi can be spotty. A service worker with pre-cached word lists would be a huge reliability win. -- [ ] Native Share API - integration Share the current prompt via iOS/Android Share Sheet or copy to clipboard -- [ ] Favorites / History - Star words you liked; see last N drawn words per session -- [ ] Word suggestions from users -- [ ] Admin editor for categories and words - A protected `/admin` page to add, edit, delete words and categories without touching MongoDB directly -- [ ] Sound Effects - Optional button click / word-reveal sounds themed to the retro aesthetic -- [ ] Multiplayer / Room Mode - Host generates a prompt and all players on the same "room" see it simultaneously (WebSocket or polling) -- [ ] QR Code Share - Generate a QR code for a specific prompt to display on a projector/screen -- [ ] Animated Word Reveal - Glitch/typewriter animation when a new word appears, matching the CRT aesthetic +- [ ] Scene Card Mode — generate a full scene prompt (Character + Location + Situation + Relation) in one tap, displayed as a card +- [ ] PWA offline mode — pre-cache word lists in the service worker for zero-latency on-stage use +- [ ] Native Share API — share the current prompt via iOS/Android Share Sheet or copy to clipboard +- [ ] Favorites / History — star words you liked; see the last N drawn words per session +- [ ] Admin editor — protected `/admin` page to add, edit, delete words and categories without touching MongoDB directly +- [ ] Sound Effects — optional button-click / word-reveal sounds themed to the retro aesthetic +- [ ] Multiplayer / Room Mode — host generates a prompt and all players in the same "room" see it simultaneously (WebSocket or long polling) +- [ ] QR Code Share — generate a QR code for a specific prompt to display on a projector/screen +- [ ] Animated Word Reveal — glitch/typewriter animation when a new word appears, matching the CRT aesthetic ---