Overview
EpiNeko includes a scoring system that allows users to rate anime in their library on a scale of 0-10, matching the MyAnimeList rating standard. Scores help you remember which series you enjoyed most and provide personal ratings for your collection.
0-10 Scale Rate anime using the standard 0-10 point scale
Optional Rating Scoring is optional - rate only what you want
Database Validated Scores validated at database level for integrity
Library Integration Scores stored with your library items
Score Field
The score is an optional integer field in library items:
export interface LibraryItem {
id ?: string ;
user_id ?: string ;
anime_id_jikan : number ;
title : string ;
image_url ?: string ;
status : LibraryStatus ;
score ?: number ; // Rating from 0-10
episodes_watched ?: number ;
}
Database Schema (supabase/migrations/20260218_initial_schema.sql:54):
create table public .user_library (
-- ...
score integer check (score >= 0 and score <= 10 ),
-- ...
);
The database constraint ensures scores are always between 0 and 10 inclusive. Invalid scores are rejected at the database level.
Rating Scale
EpiNeko uses the standard 0-10 rating scale:
Score Rating Description 10 Masterpiece Perfect in every way 9 Great Exceptional quality 8 Very Good Highly enjoyable 7 Good Worth watching 6 Fine Decent but flawed 5 Average Neither good nor bad 4 Bad Below average 3 Very Bad Significant problems 2 Horrible Almost unwatchable 1 Appalling Completely terrible 0 Unrated Not scored yet
This scale matches MyAnimeList’s rating system, making it familiar to anime fans and allowing for easy comparison.
Adding Scores
Via updateLibraryItem
Add or update a score for any anime in your library:
import { updateLibraryItem } from '@/services/library' ;
// Add score to existing library item
await updateLibraryItem ( 52991 , {
score: 9
});
// Update score along with status
await updateLibraryItem ( 52991 , {
status: 'completed' ,
score: 9 ,
episodes_watched: 28
});
When Adding to Library
Include score when first adding anime:
import { addToLibrary , LibraryItem } from '@/services/library' ;
const item : LibraryItem = {
anime_id_jikan: 52991 ,
title: "Sousou no Frieren" ,
image_url: "https://cdn.myanimelist.net/images/anime/..." ,
status: 'completed' ,
episodes_watched: 28 ,
score: 9 // Initial rating
};
await addToLibrary ( item );
Score Without Completing
You can score anime even if not completed:
// Rate after dropping
await updateLibraryItem ( animeId , {
status: 'dropped' ,
episodes_watched: 5 ,
score: 4 // Rate what you watched
});
// Rate while watching
await updateLibraryItem ( animeId , {
status: 'watching' ,
episodes_watched: 12 ,
score: 8 // Rate based on episodes so far
});
While you can score incomplete anime, it’s recommended to score only completed series for the most accurate ratings.
Removing Scores
Set score to null or undefined to remove:
// Remove score
await updateLibraryItem ( animeId , {
score: null
});
// Or omit score field entirely
await updateLibraryItem ( animeId , {
status: 'watching'
// score remains unchanged
});
Displaying Scores
In Library Cards
Show user’s personal score on library anime cards:
import AnimeCard from '@/components/anime/AnimeCard' ;
< AnimeCard
mal_id = { item . anime_id_jikan }
image = { item . image_url }
title = { item . title }
score = { item . score ?. toString () } // Personal score
/>
AnimeCard Component (src/components/anime/AnimeCard.tsx:31-35):
{ score && (
< div className = "absolute top-3 right-3 bg-black/60 backdrop-blur-md
px-2 py-1 rounded-lg border border-white/10" >
< span className = "text-yellow-400 text-xs font-bold" >
★ { score }
</ span >
</ div >
)}
MAL Score vs Personal Score
Distinguish between MyAnimeList’s score and user’s personal score:
import { JikanAnime } from '@/services/jikan' ;
interface AnimeDisplay {
malScore : number | null ; // From Jikan API
personalScore ?: number ; // From user's library
}
// Display both
console . log ( `MAL Score: ${ anime . score ?? 'N/A' } ` );
console . log ( `Your Score: ${ libraryItem . score ?? 'Not rated' } ` );
Example Display:
< div className = "scores" >
< div className = "mal-score" >
< span className = "label" > MAL Average </ span >
< span className = "value" > ★ { anime . score || 'N/A' } </ span >
</ div >
{ libraryItem ?. score && (
< div className = "personal-score" >
< span className = "label" > Your Rating </ span >
< span className = "value" > ★ { libraryItem . score } </ span >
</ div >
) }
</ div >
Simple Dropdown
Basic score selector:
< select
value = { score || 0 }
onChange = { ( e ) => handleScoreChange ( Number ( e . target . value )) }
>
< option value = { 0 } > Not Rated </ option >
{ [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]. map ( n => (
< option key = { n } value = { n } > { n } - { getRatingLabel ( n ) } </ option >
)) }
</ select >
Interactive star-based rating:
const StarRating = ({ value , onChange } : { value : number , onChange : ( n : number ) => void }) => {
return (
< div className = "flex gap-1" >
{ [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]. map ( n => (
< button
key = { n }
onClick = { () => onChange ( n ) }
className = { n <= value ? 'text-yellow-400' : 'text-zinc-600' }
>
★
</ button >
)) }
</ div >
);
};
Slider for granular control:
< input
type = "range"
min = "0"
max = "10"
step = "1"
value = { score || 0 }
onChange = { ( e ) => handleScoreChange ( Number ( e . target . value )) }
className = "range range-primary"
/>
< div className = "text-center text-2xl font-bold" >
{ score ? `★ ${ score } ` : 'Not Rated' }
</ div >
Score Statistics
Calculate statistics from your library scores:
import { getLibrary } from '@/services/library' ;
const library = await getLibrary ();
// Filter scored items
const scoredItems = library . filter ( item => item . score !== null && item . score !== undefined );
// Calculate average score
const averageScore = scoredItems . reduce (( sum , item ) => sum + ( item . score || 0 ), 0 ) / scoredItems . length ;
console . log ( `Average Score: ${ averageScore . toFixed ( 2 ) } ` );
console . log ( `Rated: ${ scoredItems . length } / ${ library . length } ` );
Score Distribution
Analyze your rating patterns:
const scoreDistribution = {
masterpiece: library . filter ( i => i . score === 10 ). length ,
great: library . filter ( i => i . score === 9 ). length ,
veryGood: library . filter ( i => i . score === 8 ). length ,
good: library . filter ( i => i . score === 7 ). length ,
fine: library . filter ( i => i . score === 6 ). length ,
average: library . filter ( i => i . score === 5 ). length ,
belowAverage: library . filter ( i => i . score && i . score < 5 ). length ,
};
// Find your highest rated anime
const topRated = library
. filter ( i => i . score )
. sort (( a , b ) => ( b . score || 0 ) - ( a . score || 0 ))
. slice ( 0 , 10 );
Visualization
Display score distribution chart:
const ScoreDistribution = ({ library } : { library : LibraryItem [] }) => {
const scores = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ];
return (
< div className = "space-y-2" >
{ scores . map ( score => {
const count = library . filter ( i => i . score === score ). length ;
const percentage = ( count / library . length ) * 100 ;
return (
< div key = { score } className = "flex items-center gap-4" >
< span className = "w-8 text-right font-bold" > { score } </ span >
< div className = "flex-1 h-6 bg-zinc-800 rounded-full overflow-hidden" >
< div
className = "h-full bg-yellow-400"
style = { { width: ` ${ percentage } %` } }
/>
</ div >
< span className = "w-12 text-zinc-400" > { count } </ span >
</ div >
);
}) }
</ div >
);
};
Scoring Recommendations
Best Practices
Score After Completion Rate anime after finishing for most accurate scores
Use Full Scale Don’t be afraid to use the entire 0-10 range
Be Consistent Develop personal criteria for each score level
Update Scores Revisit and adjust scores as your tastes evolve
Personal Rating Guidelines
Develop your own rating criteria:
Story-Focused
Entertainment
Overall Quality
10: Perfect narrative
8-9: Excellent storytelling
6-7: Good but flawed plot
4-5: Weak story
1-3: Poor narrative
10: Maximum enjoyment
8-9: Highly entertaining
6-7: Fun to watch
4-5: Somewhat boring
1-3: Not entertaining
10: Perfect in all aspects
8-9: Minor flaws only
6-7: Some issues
4-5: Major problems
1-3: Fundamentally flawed
Database Validation
Scores are validated at multiple levels:
Database Constraint
PostgreSQL check constraint:
score integer check (score >= 0 and score <= 10 )
Behavior:
Rejects scores < 0
Rejects scores > 10
Accepts null (no score)
Accepts 0-10 inclusive
Application Validation
Add client-side validation:
const validateScore = ( score : number | null | undefined ) : boolean => {
if ( score === null || score === undefined ) return true ; // Null is valid
return score >= 0 && score <= 10 && Number . isInteger ( score );
};
// Before saving
if ( ! validateScore ( score )) {
throw new Error ( 'Score must be an integer between 0 and 10' );
}
await updateLibraryItem ( animeId , { score });
Type Safety
Use TypeScript for type safety:
type Score = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | null | undefined ;
interface LibraryItem {
// ...
score ?: Score ;
}
Score-Based Features
Filtering by Score
Filter library by rating:
const library = await getLibrary ();
// Highly rated anime (8+)
const highlyRated = library . filter ( item => item . score && item . score >= 8 );
// Low rated (< 5)
const lowRated = library . filter ( item => item . score && item . score < 5 );
// Unrated
const unrated = library . filter ( item => ! item . score );
Sorting by Score
Sort library by personal ratings:
// Highest rated first
const sortedByScore = library . sort (( a , b ) =>
( b . score || 0 ) - ( a . score || 0 )
);
// Lowest rated first
const lowestFirst = library . sort (( a , b ) =>
( a . score || 0 ) - ( b . score || 0 )
);
// Unrated first, then by score
const unratedFirst = library . sort (( a , b ) => {
if ( ! a . score && ! b . score ) return 0 ;
if ( ! a . score ) return - 1 ;
if ( ! b . score ) return 1 ;
return b . score - a . score ;
});
Recommendations
Find similar highly-rated anime:
const getRecommendations = async ( animeId : number ) => {
const library = await getLibrary ();
const targetItem = library . find ( i => i . anime_id_jikan === animeId );
if ( ! targetItem ?. score ) return [];
// Find anime with similar scores
return library . filter ( item =>
item . anime_id_jikan !== animeId &&
item . score &&
Math . abs ( item . score - targetItem . score ) <= 1
);
};
Comparing Scores
MAL Score Comparison
Compare your rating to MAL average:
interface ScoreComparison {
anime : JikanAnime ;
malScore : number | null ;
personalScore : number | null ;
difference : number | null ;
}
const compareScores = ( anime : JikanAnime , libraryItem ?: LibraryItem ) : ScoreComparison => {
const malScore = anime . score ;
const personalScore = libraryItem ?. score || null ;
const difference = malScore && personalScore
? personalScore - malScore
: null ;
return {
anime ,
malScore ,
personalScore ,
difference
};
};
// Usage
const comparison = compareScores ( anime , libraryItem );
console . log ( `Your score is ${ comparison . difference } points ${
comparison . difference > 0 ? 'higher' : 'lower'
} than MAL average` );
Score Agreement Analysis
Analyze how often you agree with MAL:
const analyzeAgreement = ( library : LibraryItem [], animeData : JikanAnime []) => {
let agreements = 0 ;
let disagreements = 0 ;
library . forEach ( item => {
const anime = animeData . find ( a => a . mal_id === item . anime_id_jikan );
if ( ! anime ?. score || ! item . score ) return ;
const diff = Math . abs ( item . score - anime . score );
if ( diff <= 1 ) {
agreements ++ ;
} else {
disagreements ++ ;
}
});
return {
agreementRate: ( agreements / ( agreements + disagreements )) * 100 ,
agreements ,
disagreements
};
};
Export Scores
Export your ratings for backup or analysis:
const exportScores = async () => {
const library = await getLibrary ();
const scores = library
. filter ( item => item . score )
. map ( item => ({
animeId: item . anime_id_jikan ,
title: item . title ,
score: item . score ,
status: item . status
}));
// Convert to CSV
const csv = [
'Anime ID,Title,Score,Status' ,
... scores . map ( s => ` ${ s . animeId } ," ${ s . title } ", ${ s . score } , ${ s . status } ` )
]. join ( ' \n ' );
return csv ;
};