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
The MyAnimeList ID (Jikan ID) of the anime
Total episode count for the series. Can be null for unknown or ongoing series.
Anime title for library operations
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
Auto Add to Library
Auto Status Change
Optimistic Updates
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 );
}
Status updates automatically based on progress: // Completed when all episodes watched
if ( newCount >= totalEpisodes ) {
status = 'completed' ;
}
// Watching when some episodes watched
else if ( newCount > 0 ) {
status = 'watching' ;
}
// Plan to watch when no episodes watched
else {
status = 'plan_to_watch' ;
}
UI updates immediately before server confirmation: // Update UI first
setWatchedCount ( newCount );
setIsInLibrary ( true );
// Then sync to database
await updateLibraryItem ( animeId , { episodes_watched: newCount });
// Rollback if failed
catch ( error ) {
setWatchedCount ( oldCount );
setIsInLibrary ( false );
}
Visual Components
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
Additional metadata displayed for each episode:
Episode Number
Format: T1 E (Season 1, Episode X) Displayed in rounded pill badge
Episode Title
English or romaji title from Jikan Truncates if too long
Japanese Title
Original Japanese title (if available) Shown in smaller text below main title
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:
User clicks episode 10 checkbox
All episodes 1-10 are considered watched
Visually, all episode cards 1-10 show as completed
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:
Add to Library
Update Progress
Auto Complete
// 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:
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 ;
}