Skip to main content

Overview

EpiNeko’s progress tracking system allows users to mark individual episodes as watched, automatically updating their library statistics and watch status. The system integrates seamlessly with the library management and provides visual feedback on viewing progress.

Episode Tracking

Mark individual episodes as watched with one click

Auto Status Update

Status automatically changes based on progress

Visual Progress

Progress bars show completion percentage

Episode Metadata

View episode titles, dates, and filler indicators

Episodes Watched Field

Each library item tracks the number of episodes you’ve watched:
export interface LibraryItem {
  // ... other fields
  episodes_watched?: number;  // Current progress (0 to total)
}
Database Schema (supabase/migrations/20260218_initial_schema.sql:55):
create table public.user_library (
  -- ...
  episodes_watched integer default 0,
  -- ...
);
The episodes_watched field defaults to 0 and represents the highest episode number you’ve completed. For example, episodes_watched: 5 means you’ve watched episodes 1-5.

Episode Data Structure

Episode information is fetched from the Jikan API:
export interface JikanEpisode {
  mal_id: number;              // Episode number
  url: string;                 // MyAnimeList URL
  title: string;               // Episode title
  title_japanese: string | null;
  title_romanji: string | null;
  duration: number | null;     // Minutes
  aired: string | null;        // Air date
  filler: boolean;             // Is this a filler episode?
  recap: boolean;              // Is this a recap episode?
  forum_url: string | null;
}

EpisodeList Component

The main component for displaying and tracking episode progress:
import EpisodeList from '@/components/anime/EpisodeList';

<EpisodeList 
  animeId={52991}
  totalEpisodes={28}
  title="Sousou no Frieren"
  imageUrl="https://cdn.myanimelist.net/images/anime/..."
/>

Component Props

animeId
number
required
The MyAnimeList ID (Jikan ID) of the anime
totalEpisodes
number | null
required
Total episode count for the series. Can be null for unknown or ongoing series.
title
string
required
Anime title for library operations
imageUrl
string
required
Poster image URL for library operations

Marking Episodes as Watched

Toggle Episode Function

The core function for marking episodes:
const handleToggleEpisode = async (episodeNum: number) => {
  // Determine if adding to library or updating
  const isAdding = !isInLibrary;
  
  // Calculate new watched count
  const newCount = watchedCount === episodeNum ? episodeNum - 1 : episodeNum;
  
  // Check if completed
  const isCompleted = totalEpisodes ? newCount >= totalEpisodes : false;
  const newStatus: LibraryStatus = isCompleted ? 'completed' : 'watching';

  // Optimistic update
  setWatchedCount(newCount);
  
  try {
    if (isAdding) {
      // First time tracking - add to library
      await addToLibrary({
        anime_id_jikan: animeId,
        title: title,
        image_url: imageUrl,
        status: newStatus,
        episodes_watched: newCount,
      });
    } else {
      // Update existing item
      await updateLibraryItem(animeId, { 
        episodes_watched: newCount,
        status: newCount === 0 ? 'plan_to_watch' : newStatus
      });
    }
  } catch (error) {
    // Rollback on error
    setWatchedCount(watchedCount);
  }
};
Implementation Reference: src/components/anime/EpisodeList.tsx:44-85

Automatic Behaviors

If you mark an episode as watched and the anime isn’t in your library, it’s automatically added:
if (isAdding) {
  const item: LibraryItem = {
    anime_id_jikan: animeId,
    title: title,
    image_url: imageUrl,
    status: newStatus,
    episodes_watched: newCount,
  };
  await addToLibrary(item);
  setIsInLibrary(true);
}

Visual Components

Progress Header

Displays current progress at the top of the episode list:
<div className="flex items-center justify-between mb-8">
  <h2 className="text-2xl font-black italic border-l-4 border-primary pl-4">
    EPISODIOS
  </h2>
  <span className="text-zinc-500 text-sm font-bold bg-zinc-900 px-4 py-1 rounded-full">
    {watchedCount} / {totalEpisodes || displayEpisodes.length} VISTOS
  </span>
</div>
Features:
  • Shows watched count vs total
  • Updates in real-time as you mark episodes
  • Styled with primary border accent

Episode Cards

Each episode is displayed as an interactive card:
<div className={`
  flex items-center justify-between p-4 rounded-2xl transition-all
  ${isWatched 
    ? 'bg-emerald-500/10 border-emerald-500/30 ring-1 ring-emerald-500/20' 
    : 'bg-zinc-900/40 border-white/5 hover:border-white/10'
  }
`}>
  {/* Episode identifier */}
  <div className={`
    px-4 py-1.5 rounded-full font-black italic
    ${isWatched ? 'bg-emerald-500 text-black' : 'bg-zinc-800 text-zinc-400'}
  `}>
    T1 E{epNum}
  </div>
  
  {/* Episode title */}
  <h3 className={isWatched ? 'text-emerald-100' : 'text-zinc-200'}>
    {ep.title}
  </h3>
  
  {/* Checkbox button */}
  <button onClick={() => handleToggleEpisode(epNum)}>
    <svg><!-- Checkmark icon --></svg>
  </button>
</div>
Visual States (src/components/anime/EpisodeList.tsx:121-169):
  • Unwatched: Gray background, white/5 border
  • Watched: Green background, emerald border with glow
  • Hover: Border brightens, checkbox highlights
  • Active: Scale animation on click

Episode Information

Additional metadata displayed for each episode:
1

Episode Number

Format: T1 E (Season 1, Episode X)Displayed in rounded pill badge
2

Episode Title

English or romaji title from JikanTruncates if too long
3

Japanese Title

Original Japanese title (if available)Shown in smaller text below main title
4

Filler/Recap Indicators

Future enhancement - show badges for filler/recap episodes

Loading and Empty States

Loading State

Displays spinner while fetching episode data:
if (isLoading) {
  return (
    <div className="flex justify-center py-12">
      <span className="loading loading-spinner loading-lg text-primary"></span>
    </div>
  );
}

No Episodes Available

When Jikan has no episode data but total count is known:
const displayEpisodes = episodes.length > 0 
  ? episodes 
  : Array.from({ length: totalEpisodes || 0 }, (_, i) => ({
      mal_id: i + 1,
      title: `Episodio ${i + 1}`,
    } as JikanEpisode));
Fallback Behavior (src/components/anime/EpisodeList.tsx:96-101):
  • Generates placeholder episodes if Jikan data unavailable
  • Uses episode numbers as titles
  • Still fully functional for progress tracking

Truly Empty

No episodes and no total count:
if (displayEpisodes.length === 0) {
  return (
    <div className="bg-zinc-900/30 rounded-2xl p-8 border border-white/5 text-center">
      <p className="text-zinc-500 italic">
        No hay información de episodios disponible.
      </p>
    </div>
  );
}

Batch Operations

Mark Multiple Episodes

Mark multiple episodes at once by clicking the highest episode number:
// Clicking episode 10 marks episodes 1-10 as watched
const newCount = 10;

await updateLibraryItem(animeId, { 
  episodes_watched: newCount 
});
User Flow:
  1. User clicks episode 10 checkbox
  2. All episodes 1-10 are considered watched
  3. Visually, all episode cards 1-10 show as completed
  4. Progress counter updates: “10 / 28 VISTOS”

Unmark Episodes

Clicking the current highest episode decrements the count:
// If watchedCount is 10, clicking episode 10 sets it to 9
const newCount = watchedCount === episodeNum ? episodeNum - 1 : episodeNum;
Example:
  • Currently watched through episode 10
  • Click episode 10 again
  • Count decreases to 9
  • Episode 10 shows as unwatched

Integration with Library

Progress tracking is tightly integrated with library management:
// Marking an episode adds anime to library
await addToLibrary({
  anime_id_jikan: animeId,
  title: title,
  image_url: imageUrl,
  status: 'watching',
  episodes_watched: 1,  // First episode
});

Error Handling

Comprehensive error handling with rollback:
try {
  // Optimistic update
  setWatchedCount(newCount);
  setIsInLibrary(true);
  
  // Sync to database
  await updateLibraryItem(animeId, { episodes_watched: newCount });
  
  router.refresh();
} catch (error: any) {
  // Rollback optimistic updates
  setWatchedCount(watchedCount);
  setIsInLibrary(false);
  
  // Show user-friendly error
  if (error.message?.includes('logged in')) {
    alert('Debes iniciar sesión para marcar episodios como vistos.');
  } else {
    alert('Error al actualizar progreso: ' + error.message);
  }
}
Error Scenarios (src/components/anime/EpisodeList.tsx:73-84):
  • User not authenticated
  • Network failure
  • Database constraint violation
  • Rate limiting

Fetching Episode Data

Episodes are fetched from Jikan API:
import { getAnimeEpisodes } from '@/services/jikan';

const episodeResponse = await getAnimeEpisodes(animeId, page);

// Returns paginated episodes
console.log(episodeResponse.data);        // Episode array
console.log(episodeResponse.pagination);  // Page info
Data Fetch Pattern (src/components/anime/EpisodeList.tsx:22-42):
useEffect(() => {
  async function fetchData() {
    const [epRes, libItem] = await Promise.all([
      getAnimeEpisodes(animeId),    // Episode data
      getLibraryItem(animeId)       // Current progress
    ]);
    
    setEpisodes(epRes.data || []);
    if (libItem) {
      setWatchedCount(libItem.episodes_watched || 0);
    }
  }
  fetchData();
}, [animeId]);

Progress Visualization

Progress is visualized in multiple places:

Episode List Header

Shows fraction and percentage:
<span>
  {watchedCount} / {totalEpisodes || displayEpisodes.length} VISTOS
</span>

Profile Statistics

Progress bars on profile page:
<ProgressBar 
  label="Sousou no Frieren"
  value={watchedCount}
  total={totalEpisodes}
  color="bg-primary"
/>

Library Cards

Could add progress indicators to library cards:
<div className="progress-bar">
  <div 
    style={{ width: `${(watchedCount / totalEpisodes) * 100}%` }}
    className="bg-primary"
  />
</div>

Best Practices

Optimistic Updates

Update UI immediately for responsive feel
setCount(newCount); // First
await updateLibrary(); // Then

Always Rollback

Revert optimistic changes on error
catch (e) {
  setCount(oldCount);
}

Batch Fetch

Fetch episodes and library data in parallel
Promise.all([getEpisodes(), getLibrary()])

Loading States

Show spinners during data fetches and updates

Advanced Features

Sequential Watching

Future enhancement - enforce sequential watching:
const canMarkAsWatched = (epNum: number) => {
  return epNum === watchedCount + 1;
};

Partial Episodes

Track partial episode progress:
interface LibraryItem {
  episodes_watched: number;
  partial_episode_progress?: number; // 0-100 percentage
}

Watch History

Track when episodes were watched:
interface EpisodeHistory {
  episode_number: number;
  watched_at: string;
  rewatch_count: number;
}