Overview
EpiNeko provides a comprehensive profile system where users can view their anime statistics, manage account settings, and track their viewing habits. The profile system is built on Supabase Auth with custom profile data stored in a separate profiles table.
Profile Dashboard View personalized stats and viewing analytics
Account Settings Update profile information and preferences
Library Stats Track progress across different watch statuses
Activity Summary Monitor viewing efficiency and completion rates
Profile Structure
User profiles extend Supabase Auth with additional metadata:
interface Profile {
id : string ; // UUID matching auth.users.id
username : string ; // Unique username
full_name : string ; // Display name
avatar_url : string ; // Profile picture URL
website : string ; // Optional website link
updated_at : string ; // Last modification timestamp
}
Database Schema (supabase/migrations/20260218_initial_schema.sql:2-11):
create table public .profiles (
id uuid references auth . users on delete cascade not null primary key ,
updated_at timestamp with time zone ,
username text unique ,
full_name text ,
avatar_url text ,
website text ,
constraint username_length check (char_length(username) >= 3 )
);
Profile Statistics
Calculated Metrics
The profile page calculates real-time statistics from your library:
interface ProfileStats {
total : number ; // Total anime in library
watching : number ; // Currently watching
completed : number ; // Finished series
planToWatch : number ; // Plan to watch
dropped : number ; // Dropped series
}
Implementation (src/app/profile/page.tsx:48-56):
const library = await getLibrary ();
const stats = {
total: library . length ,
watching: library . filter ( i => i . status === 'watching' ). length ,
completed: library . filter ( i => i . status === 'completed' ). length ,
planToWatch: library . filter ( i => i . status === 'plan_to_watch' ). length ,
dropped: library . filter ( i => i . status === 'dropped' ). length ,
};
Viewing Efficiency
Track completion rate as a percentage:
const efficiency = stats . total > 0
? Math . round (( stats . completed / stats . total ) * 100 )
: 0 ;
console . log ( `Completion rate: ${ efficiency } %` );
Display (src/app/profile/page.tsx:157-163):
Large percentage display
“completado versus total” subtitle
Visual prominence on profile page
Profile Page Components
Profile Card
The left sidebar displays user identity and basic info:
Avatar
Gradient-bordered circular avatar with fallback to first letter of name
Display Name
Shows full_name or email username as fallback
Username
Displays @username handle
Member Since
Shows account creation date in Spanish format
Edit Button
Links to settings page for profile updates
Code Reference (src/app/profile/page.tsx:92-132):
< div className = "w-32 h-32 rounded-full bg-gradient-to-tr from-primary to-purple-600 p-1" >
< div className = "w-full h-full rounded-full bg-zinc-900 flex items-center justify-center" >
{ profile ?. avatar_url ? (
< img src = { profile . avatar_url } alt = { profile . full_name } />
) : (
< span className = "text-4xl font-black text-white italic" >
{ profile ?. full_name ?. charAt ( 0 ) || user . email ?. charAt ( 0 ). toUpperCase () }
</ span >
) }
</ div >
</ div >
Statistics Cards
Four summary cards showing key metrics:
Total Animes Total count of all library items Icon: 📊
Viendo Currently watching count Icon: 📺
Completado Completed series count Icon: ✅
Pendiente Plan to watch count Icon: ⏳
Component (src/app/profile/page.tsx:195-202):
function StatCard ({ label , value , icon } : { label : string , value : number , icon : string }) {
return (
< div className = "bg-zinc-900/50 backdrop-blur-md rounded-2xl p-5 border border-white/5
hover:border-primary/50 transition-all group" >
< div className = "text-2xl mb-2" > { icon } </ div >
< div className = "text-2xl font-black text-white italic" > { value } </ div >
< div className = "text-zinc-500 text-xs font-bold uppercase" > { label } </ div >
</ div >
);
}
Activity Summary
Visual progress bars showing distribution across statuses:
Progress Bars
Implementation
< ProgressBar label = "Viendo" value = { stats . watching } total = { stats . total } color = "bg-primary" />
< ProgressBar label = "Completado" value = { stats . completed } total = { stats . total } color = "bg-green-500" />
< ProgressBar label = "Plan para ver" value = { stats . planToWatch } total = { stats . total } color = "bg-yellow-500" />
< ProgressBar label = "Abandonado" value = { stats . dropped } total = { stats . total } color = "bg-red-500" />
function ProgressBar ({ label , value , total , color }) {
const percentage = total > 0 ? ( value / total ) * 100 : 0 ;
return (
< div className = "space-y-1.5" >
< div className = "flex justify-between" >
< span className = "text-zinc-400" > { label } </ span >
< span className = "text-white" > { value } </ span >
</ div >
< div className = "w-full h-2 bg-zinc-800 rounded-full" >
< div
className = { `h-full ${ color } transition-all duration-1000` }
style = { { width: ` ${ percentage } %` } }
/>
</ div >
</ div >
);
}
Located at src/app/profile/page.tsx:205-220
Quick Navigation
Two action cards linking to main sections:
Mi Biblioteca - Navigate to library page
Descubrir - Navigate to discovery/homepage
Design (src/app/profile/page.tsx:168-187):
Hover effects with color transitions
Icon arrows with background color change
Subtle descriptions of each section
Settings Page
Editable Profile Fields
Users can update their profile information:
Unique identifier, minimum 3 characters Validation: Database constraint enforces uniqueness and length
Display name shown throughout the app
Optional personal website URL
URL to profile picture image
Updating Profile
Profile updates use Supabase upsert for insert-or-update:
const updateProfile = async () => {
const { data : { user } } = await supabase . auth . getUser ();
const updates = {
id: user ?. id ,
full_name: fullname ,
username ,
website ,
avatar_url: avatarUrl ,
updated_at: new Date (). toISOString (),
};
const { error } = await supabase . from ( 'profiles' ). upsert ( updates );
if ( error ) throw error ;
};
Implementation Details (src/app/settings/page.tsx:57-82):
Uses upsert to handle both insert and update
Updates updated_at timestamp automatically
Displays success/error messages to user
Disables button during save operation
Settings form with controlled inputs:
< input
type = "text"
placeholder = "usuario_ninja"
className = "input input-bordered bg-zinc-950 border-white/10
focus:border-primary focus:ring-1 focus:ring-primary"
value = { username }
onChange = { ( e ) => setUsername ( e . target . value ) }
/>
Features (src/app/settings/page.tsx:113-169):
Username field with validation hint
Email field (disabled, cannot be changed)
Save button with loading state
Success/error alert messages
Back to profile navigation button
Security Features
Row-Level Security
Profile access is controlled by PostgreSQL policies:
-- Anyone can view profiles
create policy "Public profiles are viewable by everyone." on public . profiles
for select using (true);
-- Users can only insert their own profile
create policy "Users can insert their own profile." on public . profiles
for insert with check ( auth . uid () = id);
-- Users can only update their own profile
create policy "Users can update own profile." on public . profiles
for update using ( auth . uid () = id);
Security Guarantees (supabase/migrations/20260218_initial_schema.sql:16-23):
Users cannot modify other users’ profiles
Profile creation is automatic via trigger
All operations validated at database level
Automatic Profile Creation
New users automatically get a profile via database trigger:
create or replace function public .handle_new_user()
returns trigger as $$
begin
insert into public . profiles (id, full_name, username, avatar_url)
values (
new . id ,
new . raw_user_meta_data ->> 'full_name' ,
new . raw_user_meta_data ->> 'username' ,
new . raw_user_meta_data ->> 'avatar_url'
);
return new;
end ;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth . users
for each row execute procedure public . handle_new_user ();
How It Works (supabase/migrations/20260218_initial_schema.sql:26-42):
User signs up via Supabase Auth
Trigger fires after user creation
Profile row is automatically inserted
Metadata from signup is copied to profile
Authentication Flow
Checking Login Status
Profile and settings pages verify authentication:
const { data : { user } } = await supabase . auth . getUser ();
if ( ! user ) {
// Redirect to login or show message
return (
< div >
< h2 > Debes iniciar sesión para ver tu perfil </ h2 >
< Link href = "/login" > Iniciar Sesión </ Link >
</ div >
);
}
Implementation (src/app/profile/page.tsx:76-83):
Checks for authenticated user
Shows login prompt if not authenticated
Displays loading spinner during check
Fetching User Data
Combine auth data with profile data:
useEffect (() => {
const fetchData = async () => {
// Get authenticated user
const { data : { user } } = await supabase . auth . getUser ();
if ( user ) {
setUser ( user );
// Fetch profile data
const { data : profileData } = await supabase
. from ( 'profiles' )
. select ( '*' )
. eq ( 'id' , user . id )
. single ();
setProfile ( profileData );
// Fetch library stats
const library = await getLibrary ();
// Calculate stats...
}
};
fetchData ();
}, []);
Loading States
Both pages implement proper loading states:
Profile Loading
Settings Loading
if ( isLoading ) {
return (
< div className = "min-h-screen bg-zinc-950 flex items-center justify-center" >
< span className = "loading loading-spinner loading-lg text-primary" ></ span >
</ div >
);
}
Message System
Settings page shows feedback messages:
const [ message , setMessage ] = useState ({ type: '' , text: '' });
// After save success
setMessage ({
type: 'success' ,
text: '¡Perfil actualizado correctamente!'
});
// After error
setMessage ({
type: 'error' ,
text: error . message || 'Error al actualizar el perfil'
});
Display (src/app/settings/page.tsx:153-158):
{ message . text && (
< div className = { `alert ${
message . type === 'success'
? 'alert-success bg-green-500/10 border-green-500/50'
: 'alert-error bg-red-500/10 border-red-500/50'
} ` } >
< span > { message . text } </ span >
</ div >
)}
Best Practices
Username Validation Validate username format client-side before submission if ( username . length < 3 ) {
alert ( 'Username must be at least 3 characters' );
return ;
}
Optimistic Updates Update UI immediately while save is in progress
Error Handling Display user-friendly error messages for all failure cases
Loading States Always show loading indicators during async operations