Skip to main content

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:
1

Avatar

Gradient-bordered circular avatar with fallback to first letter of name
2

Display Name

Shows full_name or email username as fallback
3

Username

Displays @username handle
4

Member Since

Shows account creation date in Spanish format
5

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 itemsIcon: 📊

Viendo

Currently watching countIcon: 📺

Completado

Completed series countIcon: ✅

Pendiente

Plan to watch countIcon: ⏳
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:
<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" />

Quick Navigation

Two action cards linking to main sections:
  1. Mi Biblioteca - Navigate to library page
  2. 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:
username
string
required
Unique identifier, minimum 3 charactersValidation: Database constraint enforces uniqueness and length
full_name
string
Display name shown throughout the app
website
string
Optional personal website URL
avatar_url
string
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

Form Components

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):
  1. User signs up via Supabase Auth
  2. Trigger fires after user creation
  3. Profile row is automatically inserted
  4. 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:
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