Add Match Night: swipe-based movie discovery#84
Merged
Conversation
Tinder-style swipe feature for discovering new watchlist movies: independent async swiping, Criterion+TMDB-seeded deck, mutual thumbs-up auto-adds to watchlist with a match decorator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add explicit concurrency handling (transaction + status re-check for the up/up race), refill latency/failure/timeout story, drop the unused upsert semantics, note refill threshold/batch size are hardcoded for v1, and expand the testing section to cover the race and stale-swipe cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live testing showed criterion.com sits behind a Cloudflare JS challenge, so a server-side fetch to either the browse-listing pages or individual film pages never sees real HTML. Swap the scrape for a checked-in data/criterion-catalog.json (title/year pairs), resolved per-entry via the existing TMDB searchByTitle helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 bite-sized TDD tasks covering: schema/types, bundled Criterion catalog data, TMDB popular helper, deck refill with cursor-tracked sourcing, swipe/match transaction with concurrency safeguards, API routes, swipe card + page UI, nav entries, and the watchlist match badge. Self-review caught and fixed an unused REFILL_THRESHOLD constant before finalizing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 8 as originally written showed an "It's a match!" banner on the swipe page itself, which directly contradicted the spec's "no toast/notification on match" non-goal (surfacing is watchlist-only via Task 10's badge). Caught during pre-flight plan review; user confirmed the spec should govern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…atch Night - Add matchedViaSwipe boolean field to Movie model (default: false) - Add SwipeCandidate model to track swiped movies with metadata - Add Swipe model to record individual user swipe votes - Add TypeScript types: SwipeSource, SwipeStatus, SwipeVote, SwipeCandidateRecord, Swipe interface - Update Movie interface with matchedViaSwipe field - Generate migration: 20260723023828_add_match_night Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a static JSON file with 60 classic Criterion Collection films and a loader function to return them. Used by future deck-refill feature to resolve real movie data via TMDB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
data/ is gitignored for local runtime data (the mounted SQLite db). data/criterion-catalog.json is a checked-in static asset, not runtime data, so it needs an explicit exception rather than a force-add on every future edit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Added FETCH_TIMEOUT_MS (5000ms) constant and fetchWithTimeout wrapper - Updated fetchDetails to use fetchWithTimeout for request timeout - Updated searchByTitle's search call to use fetchWithTimeout - Added new fetchPopularMovies(page) function that: * Fetches popular movies from TMDB API * Resolves full details for each result via fetchDetails * Filters out any results that fail to resolve * Returns empty array on fetch error - Updated test imports to include fetchPopularMovies - Added 3 new tests for fetchPopularMovies behavior All 18 tests pass (15 existing + 3 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ards Adds getNextCandidateForUser() and recordSwipe() to match-night.ts. The swipe transaction re-checks candidate status inside prisma.$transaction before recording, so a stale/expired candidate is a safe no-op, and falls back to reading the existing Movie row on a P2002 race so a mutual thumbs-up never creates a duplicate.
- GET /api/match-night/next: returns next candidate for a user, validates user and returns 422 for invalid input - POST /api/match-night/swipe: records a swipe vote, validates user, candidateId, and vote value with 422 responses for invalid input - All 8 tests passing Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the MatchNightCard presentational component that displays a movie candidate with poster, title, year, and description. Provides thumbs-up and thumbs-down buttons for voting, with disabled state during voting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- refillCandidates() now skips any TMDB result with an empty imdbId in both the Criterion-resolution loop and the TMDB-popular loop, closing a gap where two empty-imdbId candidates could match and crash the second movie.create with an uncaught P2025 (the P2002 fallback looks up by tmdbId, which won't find a row from a failed create). - MovieCard (watched view) now renders the "It's a match! 🎉" badge when movie.matchedViaSwipe is true, matching MovieRow's existing behavior on the watchlist view, per the Match Night design spec. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Docker build was broken on main for two independent reasons, both surfaced while building the image for the Match Night release: - package-lock.json was out of sync with package.json (missing @emnapi/core/runtime entries from an earlier dependency bump), so `npm ci` failed with EUSAGE. Regenerated inside a node:24-alpine container to match the build image's platform-specific optional dependency resolution exactly. - .dockerignore excludes the whole data/ directory (mirroring the runtime-data .gitignore convention), which silently dropped data/criterion-catalog.json from the build context, breaking the Next.js build with a module-not-found in src/lib/criterion-catalog.ts. Added the same targeted exception used in .gitignore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Match Night's refill filled the Criterion half of each batch before the TMDB half, and served candidates oldest-first, so a whole run of Criterion picks always showed before any TMDB ones instead of being mixed. Shuffle the batch before insertion so sources interleave, with an id tiebreaker added to the serving query since same-batch rows can share a createdAt timestamp. Also expands the bundled Criterion catalog from 60 to 137 titles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
criterion.com blocks server-side scraping (Cloudflare), so the catalog was hand-compiled from memory (137 titles) — necessarily a small, imperfect subset. Replaced it with a cleaned, deduped export from the arrismo/criterioncollection GitHub dataset (spine/year/ title/director for the whole collection), keeping only single-film entries with a parseable year (1555 films) and dropping box-set/ anthology entries, which lack a year and wouldn't resolve to a single TMDB movie via searchByTitle anyway. Source: https://github.com/arrismo/criterioncollection Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SwipeCandidate,Swipe) and aMovie.matchedViaSwipeflag; new API routes, page, and components; sidebar/mobile-nav entries.imdbIdedge case that could crash a match, and adds the match badge to the watched view (MovieCard), which the design spec called for but an earlier pass missed..dockerignoregap that dropped the bundled catalog file from the build context).Design spec:
docs/superpowers/specs/2026-07-22-match-night-design.mdImplementation plan:
docs/superpowers/plans/2026-07-22-match-night.mdTest plan
npm run test:run— 220/220 passingnpx tsc --noEmit— cleannpm run lint— cleannpm run build— succeeds,/match-night+ new API routes present in route manifest/watchlist,/match-night,/api/health🤖 Generated with Claude Code