Skip to main content

Overview

Film Fanatic implements a sophisticated hybrid storage system for watchlists that works both with and without user authentication. The system synchronizes between local browser storage (for anonymous users) and cloud storage via Convex (for authenticated users).

Storage Architecture

Hybrid Storage Model

Local Storage

Anonymous Users
  • Data stored in browser localStorage
  • Uses Zustand for state management
  • Persists across browser sessions
  • No account required

Cloud Storage

Authenticated Users
  • Synced via Convex backend
  • Available across all devices
  • Real-time synchronization
  • Optimistic UI updates

Implementation Details

Location: src/hooks/usewatchlist.ts
The useWatchlistStore provides:
interface WatchlistStore {
  mediaState: WatchlistItem[];
  setWatchlistMembershipLocal: (id, type, inWatchlist, metadata) => void;
  setProgressStatusLocal: (id, type, progressStatus, progress, metadata) => void;
  setReactionLocal: (id, type, reaction, metadata) => void;
  setProgressLocal: (id, type, progress, metadata) => void;
}
  • Persists to localStorage under key watchlist-storage
  • Fallback to in-memory storage for server-side rendering
  • Automatic JSON serialization/deserialization
Backend schema: convex/schema.ts and convex/watchlist.tsKey mutations:
  • setWatchlistMembership - Add/remove from watchlist
  • setProgressStatus - Update watch status
  • setReaction - Set mood/reaction
  • updateProgress - Track percentage watched
  • markEpisodeWatched - Episode-level tracking
All mutations include optimistic updates for instant UI feedback.

Watchlist Data Structure

Each watchlist item contains:
type WatchlistItem = {
  title: string;              // Movie/show title
  type: "movie" | "tv";       // Media type
  external_id: string;        // TMDB ID
  image: string;              // Poster path
  rating: number;             // TMDB rating
  release_date: string;       // Release/air date
  overview?: string;          // Description
  updated_at: number;         // Last modified timestamp
  created_at: number;         // Added timestamp
  inWatchlist: boolean;       // Membership flag
  progressStatus: ProgressStatus | null;  // Watch status
  reaction: ReactionStatus | null;        // User reaction
  progress?: number;          // Percentage watched
};
Location: src/hooks/usewatchlist.ts:38-52

Watch Statuses

Progress Status

Users can mark items with one of four progress statuses:

Plan to Watch

Content the user intends to watch but hasn’t started.Value: "want-to-watch"

Watching

Currently in progress, actively watching.Value: "watching"

Completed

Finished watching entirely.Value: "finished"

Dropped

Started but chose not to continue.Value: "dropped"

Reaction Status

After watching, users can optionally add a mood/reaction:
Absolutely enjoyed the content.Value: "loved" | Icon: Heart ❤️
Reactions are independent of progress status. A user can love something they dropped or have mixed feelings about something they completed.

Watchlist Page UI

Location: src/routes/watchlist.tsx

Filter System

Primary filter tabs showing counts:
  • All - Everything except dropped items
  • Plan to Watch - Unwatched content
  • Watching - In progress
  • Completed - Finished items
  • Dropped - Discontinued content

Active Filter Display

The “Filters” button shows a badge with the number of active secondary filters. Users can:
  • Toggle filters panel
  • Reset all secondary filters at once
  • See real-time count updates as they apply filters
Implementation: src/routes/watchlist.tsx:298-316

Empty States

No Items

Shown when the watchlist is completely empty.Message: “No items in your watchlist”

No Matches

Shown when filters exclude all items.Message: “No items match your filters”

Import/Export

Location: src/hooks/usewatchlistimportexport.ts

Export Watchlist

1

Click Export Button

Located in the top-right of the watchlist page.
2

JSON File Generated

Browser downloads a JSON file containing all watchlist data:
  • All items with metadata
  • Progress statuses
  • Reactions
  • Timestamps
3

Backup Complete

File can be stored for backup or transferred to another device.

Import Watchlist

1

Click Import Button

Opens file picker dialog.
2

Select JSON File

Choose a previously exported watchlist JSON file.
3

Validation

System validates the file structure:
  • Checks for required fields
  • Identifies invalid entries
  • Shows warning if some items can’t be imported
4

Merge

Valid items are merged into existing watchlist:
  • Duplicates are skipped or updated based on timestamp
  • New items are added
  • User is notified of results
Import/export works for both local and cloud-synced watchlists. Imported data follows the same hybrid storage rules.

Watchlist Button Component

Location: src/components/watchlist-button.tsx The watchlist button appears throughout the app:

Placement

  • Homepage media cards
  • Search result cards
  • Media detail pages (movie/TV)
  • Collection pages

Visual States

Shows outline bookmark icon.Label: “Add to watchlist”

Toggle Behavior

Implementation: src/hooks/usewatchlist.ts:405-531
const toggle = useToggleWatchlistItem();

await toggle({
  title: "Inception",
  rating: 8.8,
  image: "/poster.jpg",
  id: "27205",
  media_type: "movie",
  release_date: "2010-07-16",
  overview: "..."
});
  • Automatically detects current membership
  • Adds if not in watchlist, removes if already present
  • Syncs to appropriate storage (local or cloud)
  • Provides optimistic UI update

Real-Time Sync for Authenticated Users

Optimistic Updates

All Convex mutations use optimistic updates:
  1. User Action - Clicks watchlist button
  2. Immediate UI Update - Button state changes instantly
  3. Backend Mutation - Sent to Convex server
  4. Success - UI remains updated
  5. Failure - UI reverts to previous state
Location: src/hooks/usewatchlist.ts:407-483

Multi-Device Sync

For authenticated users:
  • Changes on one device appear on all devices
  • Real-time subscription via Convex queries
  • Automatic conflict resolution (last-write-wins)
  • No manual refresh needed

Watchlist Card Display

Location: src/routes/watchlist.tsx:434-552 Each card shows:

Visual Info

  • Poster thumbnail
  • Media type badge (MOVIE/TV)
  • Star rating
  • Release date

Status Info

  • Title (truncated to 2 lines)
  • Overview (truncated to 2 lines)
  • Progress status badge
  • Reaction/mood badge

Card Actions

  1. Click Poster/Title - Navigate to media detail page
  2. Click Remove - Remove from watchlist with confirmation

Progress Calculation

For TV shows, progress is calculated based on episode watch tracking:
const progress = (watchedEpisodes / totalEpisodes) * 100;
For movies, progress is set directly:
  • 0% - Want to watch
  • 1-99% - Watching (partial playback)
  • 100% - Finished
Location: src/hooks/useWatchProgress.ts

Migration and Legacy Support

The system includes migration logic for legacy statuses: Old status → New status mapping:
  • "plan-to-watch"progressStatus: "want-to-watch", reaction: null
  • "watching"progressStatus: "watching", reaction: null
  • "completed"progressStatus: "finished", reaction: null
  • "liked"progressStatus: "finished", reaction: "liked"
  • "dropped"progressStatus: "dropped", reaction: null
Location: src/hooks/usewatchlist.ts:132-157

API Reference

Hooks

// Get full watchlist
const { watchlist, loading } = useWatchlist();

Build docs developers (and LLMs) love