# Project: MovieDice ## 1. Project Overview MovieDice is a mobile-first web app that helps friend groups collaboratively build a shared movie watchlist and — when nobody can agree on what to watch — randomly select one using an animated "Roll the Dice" mechanic. Groups join via a short invite code, add movies from a live TMDB search, and mark films as watched together. The app removes the friction of the "what should we watch?" problem by making both curation and random selection fast and fun. A public landing page lets visitors try the dice mechanic against the TMDB database before signing up, lowering the barrier to entry. Both the landing page and in-app rolls use the same horizontal poster-carousel mechanic: a strip of posters spins and snaps so a gap lands at viewport center, and the result card emerges with an animate-emerge pop-in (scale 0 → 1.08 → 1, ~500ms, posters spread ±110px on settle). ## 2. Target Users **Primary:** Friend groups, couples, roommates, or families who watch movies together and maintain an informal "we should watch this" list that currently lives in text threads or notes apps. **Secondary:** Remote groups (long-distance friends, online communities) who watch movies synchronously or asynchronously and want a shared queue. **Key traits:** - Mobile-first users — majority of interaction happens on phones - Low tolerance for signup friction — will abandon if auth is annoying - Motivated by the social/playful aspect as much as the utility ## 3. Core Value Proposition One shared list. One button to decide. No arguments. MovieDice solves group decision paralysis by combining collaborative curation (everyone adds what they want to watch) with a delightful randomizer that removes the burden of choosing. The invite-code group model means zero signup friction while still keeping lists private to the group. ## 4. Scope of Work ### In Scope (MVP) | Feature | Description | Priority | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | Landing page | Centered logo, splash text, slot-machine reel animation on Roll the Dice (3 reels spinning through ~20 automatically fetched posters from TMDB popular/top-rated, replaced on each periodic refresh), Genre Roll against TMDB (no login required), Login button, scrolling About section, 3-step how-it-works demo with alternating left-right-left alignment, TMDB attribution footer, privacy policy link, legal footer (© MovieDice, privacy links, CCPA/GDPR anchors, cookie notice). Roll result card emerges in the carousel center and stays settled until next user action. Result card has an "i" info button that opens a MoreInfoModal (plot, Add to list → /login, Watch Trailer). | Must Have | | Anonymous auth via Supabase | User picks a display name and optional avatar color; account created via `supabase.auth.signInAnonymously()` which issues a JWT for RLS-compatible sessions; persisted on device via `@supabase/ssr` cookie-based session handling | Must Have | | Recovery code | A 24-character alphanumeric code (128-bit entropy) shown once after account creation that lets users reclaim their identity on a new device; hashed with Argon2id (memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes) before storage; single-use (invalidated after successful claim); claim endpoint rate-limited (5 failed attempts per IP per 15-minute window) | Must Have | | Group creation with invite code | Creator gets a short human-readable code in WORD-WORD format (e.g., WOLF-MOON; word list: 2,000+ words, 3-8 characters each, offensive/confusing terms filtered, uppercase display, case-insensitive comparison, collision check on generation) to share; creator becomes List Admin | Must Have | | Group join via invite code | Enter code to join a group and access its shared list; regular member role assigned; join endpoint rate-limited (5-10 failed attempts per IP per 15-minute window); group join is a server-side operation via service role key (not client-side INSERT) | Must Have | | List Admin permissions | Creator can rename the list, initiate list deletion or ownership transfer (on self-removal), remove members, and regenerate the invite code | Must Have | | Regular user permissions | Members can add/remove movies, mark movies as watched, and leave the list | Must Have | | Movie search (TMDB integration) | Search bar queries TMDB via server-side API proxy (`/api/tmdb/*`) with debounce (~300ms); all calls set `include_adult=false` and apply PG-13 certification filter (see adult content policy below); results show below a separator from in-list results. Search bar styled `max-w-xl`, centered text, bold "ADD A MOVIE" placeholder, white pulsating glow (`@keyframes pulse-glow` in globals.css, `prefers-reduced-motion` honored). | Must Have | | Add/remove movie | Tap a TMDB result to add it; poster, genres, title, year, and trailer URL auto-populate (trailer URL stored in DB and refreshed periodically); added-by attribution stored | Must Have | | Poster-forward grid view | 2-column evenly-scaling grid; each card (`PosterCard`, now a full-card button) shows movie poster (full bleed, TMDB native sized URLs) with title below and meaningful `alt` text; added-by avatar overlaid top-right; green-circle checkmark overlaid top-left when watched; tapping a card opens `ListMoreInfoModal`; infinite scroll loading 12 movies initially. `RollBar` hidden when `allMovies.length === 0`. | Must Have | | Movie info modal | Tapping a poster opens `ListMoreInfoModal` (portal to document.body). Contents: poster (w500 with alt text) + title + "Added by [username]" + genre tags + Watched It button + Trailer button + Delete button (only when caller supplies `onDelete` prop — roll-result modal omits delete). Watched It: three-state idle→confirming→committed with shake-to-arm and `localWatched` display; reverse path mirrors with gray "Unwatch Movie" intermediate. Delete: red idle → "Click to confirm" → mutation + close; both armed states apply animate-shake with 4s auto-disarm; arming one disarms the other. Trailer opens stored URL in new tab. Modal uses createPortal to escape transformed ancestors; ESC closes, click-outside closes, focus trap, focus restoration on close. TMDB overview fetched per-open (no DB column; TanStack Query caches within session). | Must Have | | Genre filter | Genre tags render in `ListMoreInfoModal` and on result cards — informational only, not tappable filters. **Genre Roll is the sole filter entry point.** Deliberate scope decision (2026-05-06): tap-tag filtering was removed when ExpandedPanel was replaced by the modal; `selectedGenre` wiring on `PosterGrid` is dormant and being removed. | Must Have | | Roll the Dice | Button pinned above the movie grid (list view) or in RollSection (home); triggers the carousel roll animation; announce result via `aria-live="polite"` region; result card shows gold glow (`ring-2 ring-yellow-400/70 shadow-[0_0_32px_rgba(250,204,21,0.55)]`). `ListTeaserCard` in `ListRollCarousel` is a full `