Using Filament for Rapid Admin Panel Development in Laravel

Posted on 31 January 2025 Reading time: 23 min read
Using Filament for Rapid Admin Panel Development in Laravel

Building backends for applications can often be tedious and time-consuming. Enter Filament, a powerful admin panel builder based on the Laravel PHP Framework, that accelerates development while maintaining flexibility and customization options.

In this article, we’ll explore how to build an admin panel for an E-learning platform, demonstrating Filament’s capabilities. To make it easy to follow, this article will be published in two parts.

What We’ll Build in Part 1

In this first part, we’ll focus on building a robust admin panel for an e-learning platform that includes:

  • Course management with rich content editing
  • Student and instructor management
  • Module organization for courses
  • Analytics dashboard
  • File uploads for course materials

By the end of Part 1, you’ll have a fully functional admin panel that looks professional and provides a solid foundation for the student-facing features we’ll build in Part 2.

Prerequisites

What is Filament?

Filament is an admin panel builder for Laravel that helps you create beautiful, responsive admin interfaces with minimal effort. Instead of spending weeks building custom CRUD (Create, Read, Update, Delete) interfaces, Filament provides a robust set of tools to build them in minutes.

Our Project: E-learning Platform

We’ll build an e-learning platform with two main components:

  1. Admin Panel (built with Filament)

    • Course management
    • Student management
    • Instructor management
    • Content organisation
    • Analytics dashboard
  2. Student Frontend (built with Laravel and Livewire)

    • Course browsing
    • Enrollment system
    • Progress tracking
    • Student dashboard

This tutorial will be quite comprehensive, so we’ll split it into two parts. In Part 1, we’ll focus on building the admin panel with Filament. In Part 2, we’ll build the student-facing frontend.

Note:

If SQLite is not installed:

Creating a New Laravel Project

There are two ways to create a new Laravel project:

### Option 1: Using Laravel Installer (Recommended for this tutorial)

```bash
# Install Laravel installer globally (if not already installed)
composer global require laravel/installer

# Create new project
laravel new filament-elearning
cd filament-elearning

This method will prompt you to make choices about:

  • Database (We’ll choose SQLite)
  • Authentication starter kit (We’ll choose Breeze with Livewire/Volt)
  • Testing framework (We’ll choose Pest)

Option 2: Using Composer Create-Project

composer create-project laravel/laravel filament-elearning
cd filament-elearning

With this method, you’ll need to manually install and configure these components later. For this tutorial, we’ll use Option 1 (Laravel Installer) as it provides a more guided setup experience.

Understanding Our Technology Choices

  1. SQLite as Database

    When prompted to choose a database, we select SQLite.

Which database will your application use?
 SQLite
 MySQL
 MariaDB
 PostgreSQL
 SQL Server

Benefits of SQLite for development:

  • No separate database server needed
  • Single file storage
  • Zero configuration
  • Easy to version control
  • Perfect for learning and development

When you select SQLite, Laravel will:

  • Create the database file at database/database.sqlite
  • Configure the .env file automatically
  • Set up the necessary database connection

No additional database setup is required - it’s ready to use!

Planning Our Database Structure

Before installing Filament, let’s design and implement our database structure. For our e-learning platform, we need to store:

  • Courses and their content
  • Students and their progress
  • Instructors and their courses
  • Course modules and materials
  • Enrollments and progress tracking

Creating Our Database Structure

Let’s create our models and migrations:

php artisan make:model Course -m
php artisan make:model Module -m
php artisan make:model Student -m
php artisan make:model Instructor -m
php artisan make:model Enrollment -m

Now let’s define our migrations:

// database/migrations/{timestamp}_create_instructors_table.php
public function up()
{
    Schema::create('instructors', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->text('bio')->nullable();
        $table->string('specialization');
        $table->timestamps();
    });
}

// database/migrations/{timestamp}_create_courses_table.php
public function up()
{
    Schema::create('courses', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->string('level')->default('beginner');
        $table->decimal('price', 8, 2);
        $table->boolean('is_published')->default(false);
        $table->foreignId('instructor_id')->constrained();
        $table->integer('enrollments_count')->default(0); 
        $table->timestamps();
    });
}

// database/migrations/{timestamp}_create_modules_table.php
public function up()
{
    Schema::create('modules', function (Blueprint $table) {
        $table->id();
        $table->foreignId('course_id')->constrained()->cascadeOnDelete();
        $table->string('title');
        $table->integer('order');
        $table->text('content');
        $table->string('type'); // video, pdf, quiz
        $table->timestamps();
    });
}

// database/migrations/{timestamp}_create_students_table.php
public function up()
{
    Schema::create('students', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->text('bio')->nullable();
        $table->json('interests')->nullable();
        $table->timestamps();
    });
}

// database/migrations/{timestamp}_create_enrollments_table.php
public function up()
{
    Schema::create('enrollments', function (Blueprint $table) {
        $table->id();
        $table->foreignId('student_id')->constrained()->cascadeOnDelete();
        $table->foreignId('course_id')->constrained()->cascadeOnDelete();
        $table->timestamp('enrolled_at');
        $table->decimal('amount', 8, 2);
        $table->integer('progress')->default(0);
        $table->timestamps();
    });
}

Let’s also define our model relationships:

// app/Models/User.php
class User extends Authenticatable
{
    public function instructor()
    {
        return $this->hasOne(Instructor::class);
    }

    public function student()
    {
        return $this->hasOne(Student::class);
    }
}

// app/Models/Course.php
class Course extends Model
{
    public function instructor()
    {
        return $this->belongsTo(Instructor::class);
    }

    public function modules()
    {
        return $this->hasMany(Module::class);
    }

    public function enrollments()
    {
        return $this->hasMany(Enrollment::class);
    }

    public function students()
    {
        return $this->belongsToMany(Student::class, 'enrollments');
    }
}

// app/Models/Module.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Module extends Model
{
    protected $fillable = [
        'course_id',
        'title',
        'content',
        'type',
        'order',
    ];

    public function course(): BelongsTo
    {
        return $this->belongsTo(Course::class);
    }
}

// app/Models/Student.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Student extends Model
{
    protected $fillable = [
        'user_id',
        'bio',
        'interests',
    ];

    protected $casts = [
        'interests' => 'array',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function enrollments(): HasMany
    {
        return $this->hasMany(Enrollment::class);
    }

    public function courses(): BelongsToMany
    {
        return $this->belongsToMany(Course::class, 'enrollments')
            ->withPivot('enrolled_at', 'amount', 'progress')
            ->withTimestamps();
    }
}

// app/Models/Instructor.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Instructor extends Model
{
    protected $fillable = [
        'user_id',
        'bio',
        'specialization',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function courses(): HasMany
    {
        return $this->hasMany(Course::class);
    }
}

// app/Models/Enrollment.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Enrollment extends Model
{
    protected $fillable = [
        'student_id',
        'course_id',
        'enrolled_at',
        'amount',
        'progress',
    ];

    protected $casts = [
        'enrolled_at' => 'datetime',
        'amount' => 'decimal:2',
        'progress' => 'integer',
    ];

    public function student(): BelongsTo
    {
        return $this->belongsTo(Student::class);
    }

    public function course(): BelongsTo
    {
        return $this->belongsTo(Course::class);
    }
}


Note:

Remember to define the appropriate $fillable or $guarded properties for your models to enable mass assignment when needed.

Run the migrations:

php artisan migrate

Let us create Seeders to populate our database with sample data:

// database/seeders/UserSeeder.php
namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UserSeeder extends Seeder
{
    public function run(): void
    {
        // Create admin user
        User::create([
            'name' => 'Admin User',
            'email' => '[email protected]',
            'password' => Hash::make('password'),
            'email_verified_at' => now(),
        ]);

        // Create sample users for instructors and students
        User::factory()
            ->count(50)
            ->create();
    }
}
// database/seeders/InstructorSeeder.php
namespace Database\Seeders;

use App\Models\Instructor;
use App\Models\User;
use Illuminate\Database\Seeder;

class InstructorSeeder extends Seeder
{
    public function run(): void
    {
        $instructors = [
            [
                'name' => 'Dr. Sarah Johnson',
                'email' => '[email protected]',
                'bio' => 'PhD in Computer Science with 10+ years of teaching experience. Specializes in web development and software architecture.',
                'specialization' => 'Web Development'
            ],
            [
                'name' => 'Prof. Michael Chen',
                'email' => '[email protected]',
                'bio' => 'Former Tech Lead at Google, now teaching full-time. Expert in machine learning and AI applications.',
                'specialization' => 'Machine Learning'
            ],
            [
                'name' => 'Jessica Williams',
                'email' => '[email protected]',
                'bio' => 'Full-stack developer with 8 years of industry experience. Passionate about teaching modern JavaScript frameworks.',
                'specialization' => 'Frontend Development'
            ],
            [
                'name' => 'David Kumar',
                'email' => '[email protected]',
                'bio' => 'Cloud architecture specialist with AWS certification. Helps companies transition to cloud infrastructure.',
                'specialization' => 'Cloud Computing'
            ],
            [
                'name' => 'Maria Garcia',
                'email' => '[email protected]',
                'bio' => 'Mobile development expert specializing in iOS and Android. Former mobile lead at Twitter.',
                'specialization' => 'Mobile Development'
            ]
        ];

        foreach ($instructors as $instructor) {
            $user = User::create([
                'name' => $instructor['name'],
                'email' => $instructor['email'],
                'password' => Hash::make('password'),
                'email_verified_at' => now(),
            ]);

            Instructor::create([
                'user_id' => $user->id,
                'bio' => $instructor['bio'],
                'specialization' => $instructor['specialization'],
            ]);
        }
    }
}

// database/seeders/CourseSeeder.php
namespace Database\Seeders;

use App\Models\Course;
use App\Models\Module;
use App\Models\Instructor;
use Illuminate\Database\Seeder;

class CourseSeeder extends Seeder
{
    public function run(): void
    {
        $courses = [
            [
                'title' => 'Modern JavaScript Development',
                'description' => 'Master modern JavaScript including ES6+, async/await, and modern tooling. Learn to build professional web applications.',
                'level' => 'intermediate',
                'price' => 149.99,
                'modules' => [
                    [
                        'title' => 'ES6+ Features',
                        'type' => 'video',
                        'content' => 'Learn about arrow functions, destructuring, and other ES6+ features.',
                    ],
                    [
                        'title' => 'Async Programming',
                        'type' => 'video',
                        'content' => 'Understanding Promises, async/await, and handling asynchronous operations.',
                    ],
                    [
                        'title' => 'Modern Tooling',
                        'type' => 'pdf',
                        'content' => 'Comprehensive guide to Webpack, Babel, and modern JavaScript tooling.',
                    ],
                ]
            ],
            [
            'title' => 'AWS Cloud Architecture',
            'description' => 'Learn to design and implement scalable cloud solutions using AWS services. Includes hands-on projects and real-world scenarios.',
            'level' => 'advanced',
            'price' => 299.99,
            'modules' => [
                [
                    'title' => 'AWS Fundamentals',
                    'type' => 'video',
                    'content' => 'Introduction to AWS services and cloud concepts.',
                ],
                [
                    'title' => 'Serverless Architecture',
                    'type' => 'video',
                    'content' => 'Building applications using AWS Lambda and API Gateway.',
                ],
                [
                    'title' => 'High Availability Design',
                    'type' => 'pdf',
                    'content' => 'Designing fault-tolerant and scalable architectures.',
                ],
                [
                    'title' => 'AWS Security Best Practices',
                    'type' => 'quiz',
                    'content' => 'Security concepts and implementation in AWS.',
                ],
            ]
            ],
            [
                'title' => 'iOS App Development with Swift',
                'description' => 'Complete guide to building iOS applications using Swift and SwiftUI. From basics to App Store deployment.',
                'level' => 'intermediate',
                'price' => 199.99,
                'modules' => [
                    [
                        'title' => 'Swift Programming Basics',
                        'type' => 'video',
                        'content' => 'Core concepts of Swift programming language.',
                    ],
                    [
                        'title' => 'UIKit Fundamentals',
                        'type' => 'video',
                        'content' => 'Building user interfaces with UIKit.',
                    ],
                    [
                        'title' => 'SwiftUI Introduction',
                        'type' => 'video',
                        'content' => 'Modern UI development with SwiftUI.',
                    ],
                    [
                        'title' => 'App Store Submission',
                        'type' => 'pdf',
                        'content' => 'Guide to preparing and submitting your app.',
                    ],
                ]
            ],
            [
                'title' => 'Full-Stack React & Node.js',
                'description' => 'Build complete web applications using React for frontend and Node.js for backend. Includes authentication, databases, and deployment.',
                'level' => 'intermediate',
                'price' => 249.99,
                'modules' => [
                    [
                        'title' => 'React Fundamentals',
                        'type' => 'video',
                        'content' => 'Core concepts of React including hooks and context.',
                    ],
                    [
                        'title' => 'Node.js & Express',
                        'type' => 'video',
                        'content' => 'Building REST APIs with Express.js.',
                    ],
                    [
                        'title' => 'MongoDB Integration',
                        'type' => 'pdf',
                        'content' => 'Working with MongoDB and Mongoose.',
                    ],
                    [
                        'title' => 'Authentication & Authorization',
                        'type' => 'video',
                        'content' => 'Implementing JWT authentication.',
                    ],
                    [
                        'title' => 'Deployment Strategies',
                        'type' => 'quiz',
                        'content' => 'Different ways to deploy your full-stack application.',
                    ],
                ]
            ],
            [
                'title' => 'Machine Learning Fundamentals',
                'description' => 'Introduction to machine learning concepts and practical applications using Python and popular ML libraries.',
                'level' => 'beginner',
                'price' => 199.99,
                'modules' => [
                    [
                        'title' => 'Introduction to Python for ML',
                        'type' => 'video',
                        'content' => 'Python basics and essential libraries for machine learning.',
                    ],
                    [
                        'title' => 'Supervised Learning',
                        'type' => 'quiz',
                        'content' => 'Quiz on classification and regression concepts.',
                    ],
                ]
            ],
        ];

        $instructors = Instructor::all();

        foreach ($courses as $index => $courseData) {
            $course = Course::create([
                'title' => $courseData['title'],
                'description' => $courseData['description'],
                'level' => $courseData['level'],
                'price' => $courseData['price'],
                'instructor_id' => $instructors[$index % count($instructors)]->id,
                'is_published' => true,
            ]);

            foreach ($courseData['modules'] as $orderIndex => $moduleData) {
                Module::create([
                    'course_id' => $course->id,
                    'title' => $moduleData['title'],
                    'type' => $moduleData['type'],
                    'content' => $moduleData['content'],
                    'order' => $orderIndex + 1,
                ]);
            }
        }
    }
}
// database/seeders/StudentSeeder.php
namespace Database\Seeders;

use App\Models\Student;
use App\Models\User;
use Illuminate\Database\Seeder;

class StudentSeeder extends Seeder
{
    private $interests = [
        'Web Development',
        'Mobile Development',
        'Machine Learning',
        'Data Science',
        'Cloud Computing',
        'DevOps',
        'UI/UX Design',
        'Blockchain',
        'Cybersecurity',
        'Game Development'
    ];

    public function run(): void
    {
        // Get users who aren't instructors
        $users = User::whereDoesntHave('instructor')->get();

        foreach ($users as $user) {
            // Randomly select 2-4 interests for each student
            $studentInterests = collect($this->interests)
                ->random(rand(2, 4))
                ->values()
                ->toArray();

            Student::create([
                'user_id' => $user->id,
                'bio' => fake()->paragraph(),
                'interests' => $studentInterests,
            ]);
        }
    }
}


// database/seeders/EnrollmentSeeder.php
namespace Database\Seeders;

use App\Models\Course;
use App\Models\Student;
use App\Models\Enrollment;
use Illuminate\Database\Seeder;
use Carbon\Carbon;

class EnrollmentSeeder extends Seeder
{
    public function run(): void
    {
        $students = Student::all();
        $courses = Course::all();

        foreach ($students as $student) {
            // Each student enrolls in 2-5 random courses
            $randomCourses = $courses->random(rand(2, 5));
            
            foreach ($randomCourses as $course) {
                // Create enrollment with realistic dates and progress
                $enrolledAt = Carbon::now()->subDays(rand(1, 180));
                
                // Calculate progress based on enrollment date
                $daysSinceEnrolled = Carbon::now()->diffInDays($enrolledAt);
                $progress = min(100, round($daysSinceEnrolled / 2)); // Roughly 2 days per 1% progress

                Enrollment::create([
                    'student_id' => $student->id,
                    'course_id' => $course->id,
                    'enrolled_at' => $enrolledAt,
                    'amount' => $course->price,
                    'progress' => $progress
                ]);
            }
        }
    }
}

Before we run the seeders, let’s update the DatabaseSeeder.php file to include our seeders:

// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            UserSeeder::class,
            InstructorSeeder::class,
            CourseSeeder::class,
            StudentSeeder::class,
            EnrollmentSeeder::class,
        ]);
    }
}

Now we can run the seeders to populate our database:

php artisan migrate:fresh --seed
Note:

Remember that the migration and seeder above would have removed your existing data. If you want to keep your existing data so your initial Filament admin user would no work. Use the admin user created in he UserSeeder to login to the admin panel.

The above seeders will provide us with data for the following:

  • 5 instructors with detailed profiles
  • 45 students with varying interests
  • 6+ detailed courses with modules
  • Realistic enrollment patterns and progress
  • Data spanning the last 6 months
Note:

The seeded data will help you visualize how the dashboard widgets and filters work with realistic data patterns. Feel free to modify the seeders to add more variations or specific test cases.

Now that our database structure is in place, we can install Filament and start building our admin panel.

Installing Filament (v3)

composer require filament/filament:"^3.2"

php artisan filament:install --panels
Note:

We’ll accept the default ID ‘admin’ as it:

  • Creates a logical URL prefix for our admin panel (/admin)
  • Follows common naming conventions
  • Makes it clear which part of the application we’re accessing
  • Can be easily protected with middleware.

This means your admin panel will be accessible at http://localhost:8000/admin if you are run php artisan serve or http://filament-elearning.test/admin if you are using Laravel Herd or Laravel Valet.

You have the option of creating the admin user in the UserSeeder like we did or you can create it using the command below:

php artisan make:filament-user

If you followed the steps correctly up to this point, you should now have a working admin panel at /admin with a login screen as shown below. Filament Admin Panel Login

Setting Up File Handling

When building an e-learning platform, we’ll need to handle various types of files such as:

  • Course cover images
  • PDF course materials
  • Video thumbnails
  • Instructor profile photos

For this, we’ll install two packages:

# Install Filament's Media Library plugin
composer require filament/spatie-laravel-media-library-plugin:"^3.0"

# Install Spatie's Media Library package
composer require spatie/laravel-medialibrary

Creating Filament Resources

Filament resources are a way to define how your models are managed within the Filament admin panel. A resource in Filament consists of a structured class that controls the CRUD (Create, Read, Update, Delete) operations, table columns, forms, and permissions for a given Eloquent model.

When you generate a Filament resource, it automatically scaffolds:

  • A table view for listing records with filters and actions.
  • A form view for creating and editing records.
  • An edit view for updating existing records.

Filament resources provide a clean, extensible approach to building admin interfaces with minimal effort.

Now, let’s create Filament resources for our models by running the following artisan commands:

php artisan make:filament-resource Course
php artisan make:filament-resource Module
php artisan make:filament-resource Student
php artisan make:filament-resource Instructor

Let’s customize the Course resource first:

// app/Filament/Resources/CourseResource.php

namespace App\Filament\Resources;

use App\Filament\Resources\CourseResource\Pages;
use App\Filament\Resources\CourseResource\RelationManagers;
use App\Models\Course;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class CourseResource extends Resource
{
    protected static ?string $model = Course::class;
    protected static ?string $navigationIcon = 'heroicon-o-academic-cap';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\Section::make()
                    ->schema([
                        Forms\Components\TextInput::make('title')
                            ->required()
                            ->maxLength(255),
                        Forms\Components\RichEditor::make('description')
                            ->required(),
                        Forms\Components\Select::make('level')
                            ->options([
                                'beginner' => 'Beginner',
                                'intermediate' => 'Intermediate',
                                'advanced' => 'Advanced',
                            ])
                            ->required(),
                        Forms\Components\TextInput::make('price')
                            ->required()
                            ->numeric()
                            ->prefix('$'),
                        Forms\Components\Select::make('instructor_id')
                            ->relationship('instructor', 'name')
                            ->required(),
                        Forms\Components\Toggle::make('is_published')
                            ->default(false),
                    ])
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('title')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('instructor.name')
                    ->sortable(),
                Tables\Columns\TextColumn::make('level')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'beginner' => 'success',
                        'intermediate' => 'warning',
                        'advanced' => 'danger',
                    }),
                Tables\Columns\TextColumn::make('price')
                    ->money('usd')
                    ->sortable(),
                Tables\Columns\IconColumn::make('is_published')
                    ->boolean(),
            ])
            ->filters([
                Tables\Filters\SelectFilter::make('level')
                    ->options([
                        'beginner' => 'Beginner',
                        'intermediate' => 'Intermediate',
                        'advanced' => 'Advanced',
                    ]),
                Tables\Filters\Filter::make('is_published')
                    ->query(fn (Builder $query): Builder => $query->where('is_published', true))
                    ->label('Published Courses Only')
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
            ])
            ->bulkActions([
                Tables\Actions\DeleteBulkAction::make(),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListCourses::route('/'),
            'create' => Pages\CreateCourse::route('/create'),
            'edit' => Pages\EditCourse::route('/{record}/edit'),
        ];
    }
}

Setting Up the Instructor Resource

Let’s customize our Instructor resource to manage instructor profiles:

// app/Filament/Resources/InstructorResource.php
namespace App\Filament\Resources;

use App\Filament\Resources\InstructorResource\Pages;
use App\Filament\Resources\InstructorResource\RelationManagers;
use App\Models\Instructor;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;


class InstructorResource extends Resource
{
    protected static ?string $model = Instructor::class;
    protected static ?string $navigationIcon = 'heroicon-o-users';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\Card::make()
                    ->schema([
                        Forms\Components\Select::make('user_id')
                            ->relationship('user', 'name')
                            ->required()
                            ->createOptionForm([
                                Forms\Components\TextInput::make('name')
                                    ->required()
                                    ->maxLength(255),
                                Forms\Components\TextInput::make('email')
                                    ->email()
                                    ->required()
                                    ->maxLength(255),
                                Forms\Components\TextInput::make('password')
                                    ->password()
                                    ->required()
                                    ->maxLength(255),
                            ]),
                        Forms\Components\Textarea::make('bio')
                            ->maxLength(500),
                        Forms\Components\TextInput::make('specialization')
                            ->required()
                            ->maxLength(255),
                    ])
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('user.name')
                    ->label('Name')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('specialization')
                    ->searchable(),
                Tables\Columns\TextColumn::make('courses_count')
                    ->counts('courses')
                    ->label('Courses'),
                Tables\Columns\TextColumn::make('created_at')
                    ->dateTime()
                    ->sortable(),
            ])
            ->filters([
                Tables\Filters\SelectFilter::make('specialization')
                    ->options(fn () => Instructor::distinct()->pluck('specialization', 'specialization')),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            RelationManagers\CoursesRelationManager::make(),
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListInstructors::route('/'),
            'create' => Pages\CreateInstructor::route('/create'),
            'edit' => Pages\EditInstructor::route('/{record}/edit'),
        ];
    }
}

Let’s create the Courses Relation Manager for instructors:

// app/Filament/Resources/InstructorResource/RelationManagers/CoursesRelationManager.php
namespace App\Filament\Resources\InstructorResource\RelationManagers;

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;

class CoursesRelationManager extends RelationManager
{
    protected static string $relationship = 'courses';

    protected static ?string $recordTitleAttribute = 'title';

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('title')
                    ->required()
                    ->maxLength(255),
                Forms\Components\RichEditor::make('description')
                    ->required(),
                Forms\Components\Select::make('level')
                    ->options([
                        'beginner' => 'Beginner',
                        'intermediate' => 'Intermediate',
                        'advanced' => 'Advanced',
                    ])
                    ->required(),
                Forms\Components\TextInput::make('price')
                    ->required()
                    ->numeric()
                    ->prefix('$'),
                Forms\Components\Toggle::make('is_published')
                    ->default(false),
            ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('title'),
                Tables\Columns\TextColumn::make('level')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'beginner' => 'success',
                        'intermediate' => 'warning',
                        'advanced' => 'danger',
                    }),
                Tables\Columns\TextColumn::make('price')
                    ->money('usd'),
                Tables\Columns\IconColumn::make('is_published')
                    ->boolean(),
            ])
            ->filters([
                //
            ])
            ->headerActions([
                Tables\Actions\CreateAction::make(),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
                Tables\Actions\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\Actions\DeleteBulkAction::make(),
            ]);
    }
}
Tip:

The relation manager allows us to manage an instructor’s courses directly from their edit page, providing a smoother workflow for administrators.

Setting Up the Module Resource

Now let’s set up the Module resource with proper ordering and course relationship:

// app/Filament/Resources/ModuleResource.php
namespace App\Filament\Resources;

use App\Filament\Resources\ModuleResource\Pages;
use App\Models\Module;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

class ModuleResource extends Resource
{
    protected static ?string $model = Module::class;
    protected static ?string $navigationIcon = 'heroicon-o-book-open';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\Section::make()
                    ->schema([
                        Forms\Components\Select::make('course_id')
                            ->relationship('course', 'title')
                            ->required(),
                        Forms\Components\TextInput::make('title')
                            ->required()
                            ->maxLength(255),
                        Forms\Components\Select::make('type')
                            ->options([
                                'video' => 'Video',
                                'pdf' => 'PDF Document',
                                'quiz' => 'Quiz',
                            ])
                            ->required(),
                        Forms\Components\RichEditor::make('content')
                            ->required()
                            ->columnSpanFull(),
                        Forms\Components\TextInput::make('order')
                            ->integer()
                            ->default(1)
                            ->required(),
                    ])
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('course.title')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('title')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('type')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'video' => 'primary',
                        'pdf' => 'success',
                        'quiz' => 'warning',
                    }),
                Tables\Columns\TextColumn::make('order')
                    ->sortable(),
            ])
            ->defaultSort('order', 'asc')
            ->filters([
                Tables\Filters\SelectFilter::make('course')
                    ->relationship('course', 'title'),
                Tables\Filters\SelectFilter::make('type')
                    ->options([
                        'video' => 'Video',
                        'pdf' => 'PDF Document',
                        'quiz' => 'Quiz',
                    ]),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
                Tables\Actions\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\Actions\DeleteBulkAction::make(),
            ])
            ->reorderable('order');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListModules::route('/'),
            'create' => Pages\CreateModule::route('/create'),
            'edit' => Pages\EditModule::route('/{record}/edit'),
        ];
    }
}
Note:

Notice how we’ve enabled reordering of modules using Filament’s reorderable() method. This allows administrators to drag and drop modules to change their order within a course.

Let’s add a Modules relation manager to our Course resource:

// app/Filament/Resources/CourseResource.php

public static function getRelations(): array
{
    return [
        RelationManagers\ModulesRelationManager::make(),
    ];
}

// app/Filament/Resources/CourseResource/RelationManagers/ModulesRelationManager.php
namespace App\Filament\Resources\CourseResource\RelationManagers;

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;

class ModulesRelationManager extends RelationManager
{
    protected static string $relationship = 'modules';

    protected static ?string $recordTitleAttribute = 'title';

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('title')
                    ->required()
                    ->maxLength(255),
                Forms\Components\Select::make('type')
                    ->options([
                        'video' => 'Video',
                        'pdf' => 'PDF Document',
                        'quiz' => 'Quiz',
                    ])
                    ->required(),
                Forms\Components\RichEditor::make('content')
                    ->required(),
                Forms\Components\TextInput::make('order')
                    ->integer()
                    ->default(fn ($livewire) => $livewire->ownerRecord->modules()->count() + 1)
                    ->required(),
            ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('title'),
                Tables\Columns\TextColumn::make('type')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'video' => 'primary',
                        'pdf' => 'success',
                        'quiz' => 'warning',
                    }),
                Tables\Columns\TextColumn::make('order'),
            ])
            ->defaultSort('order', 'asc')
            ->reorderable('order');
    }
}

Setting Up the Student Resource

Let’s create a comprehensive student management interface with enrollment tracking:

// app/Filament/Resources/StudentResource.php
namespace App\Filament\Resources;

use App\Filament\Resources\StudentResource\Pages;
use App\Filament\Resources\StudentResource\RelationManagers\EnrollmentsRelationManager;
use App\Models\Student;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class StudentResource extends Resource
{
    protected static ?string $model = Student::class;
    protected static ?string $navigationIcon = 'heroicon-o-user-group';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\Section::make()
                    ->schema([
                        Forms\Components\Select::make('user_id')
                            ->relationship('user', 'name')
                            ->required()
                            ->createOptionForm([
                                Forms\Components\TextInput::make('name')
                                    ->required()
                                    ->maxLength(255),
                                Forms\Components\TextInput::make('email')
                                    ->email()
                                    ->required()
                                    ->maxLength(255),
                                Forms\Components\TextInput::make('password')
                                    ->password()
                                    ->required()
                                    ->maxLength(255),
                            ]),
                        Forms\Components\Textarea::make('bio')
                            ->maxLength(500),
                        Forms\Components\TagsInput::make('interests')
                            ->suggestions([
                                'Web Development',
                                'Mobile Development',
                                'Data Science',
                                'DevOps',
                                'UI/UX Design',
                                'Machine Learning',
                            ]),
                    ])
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('user.name')
                    ->label('Name')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('user.email')
                    ->label('Email')
                    ->searchable(),
                Tables\Columns\TextColumn::make('interests')
                    ->listWithLineBreaks()
                    ->bulleted(),
                Tables\Columns\TextColumn::make('enrollments_count')
                    ->counts('enrollments')
                    ->label('Enrolled Courses'),
                Tables\Columns\TextColumn::make('created_at')
                    ->dateTime()
                    ->sortable(),
            ])
            ->filters([
                Tables\Filters\Filter::make('has_enrollments')
                    ->query(fn (Builder $query) => $query->has('enrollments')),
                Tables\Filters\Filter::make('no_enrollments')
                    ->query(fn (Builder $query) => $query->doesntHave('enrollments')),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
                Tables\Actions\Action::make('view_progress')
                    ->icon('heroicon-o-chart-bar')
                    ->url(fn (Student $record): string =>
                    StudentResource::getUrl('progress', ['record' => $record->getKey()]))
                    ->openUrlInNewTab(),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            EnrollmentsRelationManager::make(),
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListStudents::route('/'),
            'create' => Pages\CreateStudent::route('/create'),
            'edit' => Pages\EditStudent::route('/{record}/edit'),
            'progress' => Pages\ViewStudentProgress::route('/{record}/progress'),
        ];
    }
}
Tip:

Notice how we use the ProgressColumn to display the course progress visually. Filament provides many column types out of the box to create rich, informative interfaces.

Let’s create the Enrollments relation manager for students:

// app/Filament/Resources/StudentResource/RelationManagers/EnrollmentsRelationManager.php
namespace App\Filament\Resources\StudentResource\RelationManagers;

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;

class EnrollmentsRelationManager extends RelationManager
{
    protected static string $relationship = 'enrollments';

    protected static ?string $recordTitleAttribute = 'course.title';

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\Select::make('course_id')
                    ->relationship('course', 'title')
                    ->required(),
                Forms\Components\DateTimePicker::make('enrolled_at')
                    ->required()
                    ->default(now()),
                Forms\Components\TextInput::make('amount')
                    ->numeric()
                    ->required()
                    ->prefix('$'),
                Forms\Components\TextInput::make('progress')
                    ->numeric()
                    ->default(0)
                    ->minValue(0)
                    ->maxValue(100)
                    ->suffix('%')
                    ->required(),
            ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('course.title')
                    ->searchable()
                    ->sortable(),
                Tables\Columns\TextColumn::make('enrolled_at')
                    ->dateTime()
                    ->sortable(),
                Tables\Columns\TextColumn::make('amount')
                    ->money('usd')
                    ->sortable(),
                Tables\Columns\TextColumn::make('progress')
                    ->badge()
                    ->color(fn (int $state): string => match (true) {
                        $state >= 80 => 'success',
                        $state >= 50 => 'warning',
                        default => 'danger',
                    })
                    ->formatStateUsing(fn (int $state): string => "{$state}%"),
            ])
            ->defaultSort('enrolled_at', 'desc')
            ->filters([
                Tables\Filters\SelectFilter::make('course')
                    ->relationship('course', 'title'),
                Tables\Filters\Filter::make('completed')
                    ->query(fn ($query) => $query->where('progress', 100)),
                Tables\Filters\Filter::make('in_progress')
                    ->query(fn ($query) => $query->where('progress', '<', 100)),
            ])
            ->headerActions([
                Tables\Actions\CreateAction::make(),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
                Tables\Actions\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\Actions\DeleteBulkAction::make(),
            ]);
    }
}

Let’s also add a custom page to view detailed student progress:

// app/Filament/Resources/StudentResource/Pages/ViewStudentProgress.php
namespace App\Filament\Resources\StudentResource\Pages;

use Filament\Resources\Pages\Page;
use App\Filament\Resources\StudentResource;


class ViewStudentProgress extends Page
{
    protected static string $resource = StudentResource::class;

    protected static string $view = 'filament.resources.students.pages.view-student-progress';

    public static function getRoutes(): array
    {
        return [
            Page::route('/{record}/progress', static::class),
        ];
    }
}

Creating the Student Progress View

First, let’s create the view file:

// resources/views/filament/resources/students/pages/view-student-progress.blade.php
// resources/views/filament/resources/student/pages/view-student-progress.blade.php
<x-filament::page>
    <x-filament::card>
        <div class="space-y-6">
            <h2 class="text-lg font-medium">Student Progress</h2>

            <div>
                <livewire:student-progress-chart :student="$record" />
            </div>

            {{-- Course Progress Details --}}
            <div class="mt-6">
                <h3 class="text-base font-medium mb-4">Course Progress Details</h3>
                @foreach($record->enrollments as $enrollment)
                    <div class="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
                        <div class="flex justify-between items-center">
                            <div>
                                <h4 class="font-medium">{{ $enrollment->course->title }}</h4>
                                <p class="text-sm text-gray-500">
                                    Enrolled: {{ $enrollment->enrolled_at->format('M d, Y') }}
                                </p>
                            </div>
                            <div class="text-right">
                                <div class="text-lg font-bold">{{ $enrollment->progress }}%</div>
                                <div class="text-sm text-gray-500">Complete</div>
                            </div>
                        </div>
                    </div>
                @endforeach
            </div>
        </div>
    </x-filament::card>
</x-filament::page>

Now let’s create the Livewire components for the charts and activity:

// app/Http/Livewire/StudentProgressChart.php
namespace App\Http\Livewire;

use Livewire\Component;
use Filament\Widgets\ChartWidget;

class StudentProgressChart extends ChartWidget
{
    public $student;

    protected static ?string $heading = 'Course Progress';

    protected function getData(): array
    {
        $enrollments = $this->student->enrollments()
            ->with('course')
            ->orderBy('enrolled_at')
            ->get();

        return [
            'datasets' => [
                [
                    'label' => 'Progress',
                    'data' => $enrollments->map(fn ($enrollment) => $enrollment->progress),
                    'borderColor' => '#3b82f6',
                    'fill' => false,
                ],
            ],
            'labels' => $enrollments->map(fn ($enrollment) => $enrollment->course->title),
        ];
    }

    protected function getType(): string
    {
        return 'line';
    }

    protected function getOptions(): array
    {
        return [
            'plugins' => [
                'legend' => [
                    'display' => true,
                ],
            ],
            'scales' => [
                'y' => [
                    'min' => 0,
                    'max' => 100,
                    'ticks' => [
                        'callback' => '##function(value) { return value + "%"; }',
                    ],
                ],
            ],
        ];
    }
}

Customizing the Admin Dashboard

Now let’s create a custom dashboard that provides an overview of the platform:

// app/Filament/Pages/Dashboard.php
namespace App\Filament\Pages;

use App\Filament\Widgets\InstructorPerformance;
use App\Filament\Widgets\RecentEnrollments;
use App\Filament\Widgets\StatsOverview;
use Filament\Pages\Dashboard as BaseDashboard;

class Dashboard extends BaseDashboard
{
    protected function getHeaderWidgets(): array
    {
       return [
            StatsOverview::class,
        ];
    }

    protected function getFooterWidgets(): array
    {
        return [
            RecentEnrollments::class,
            InstructorPerformance::class,
        ];
    }
}

Let’s add widgets for Stats Overview, Recent Enrollments and Instructor performance:

// app/Filament/Widgets/StatsOverview.php
namespace App\Filament\Widgets;

use App\Models\Course;
use App\Models\Enrollment;
use App\Models\Student;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Card;

class StatsOverview extends StatsOverviewWidget
{
    protected function getCards(): array
    {
        return [
            Card::make('Total Students', Student::count())
                ->description('Total registered students')
                ->descriptionIcon('heroicon-s-user-group')
                ->chart([7, 12, 16, 18, 22, 27, 30])
                ->color('success'),

            Card::make('Active Courses', Course::where('is_published', true)->count())
                ->description('Published courses')
                ->descriptionIcon('heroicon-s-academic-cap')
                ->chart([10, 12, 12, 13, 15, 17, 18])
                ->color('primary'),

            Card::make('Total Revenue', '$' . number_format(Enrollment::sum('amount'), 2))
                ->description('From all enrollments')
                ->descriptionIcon('heroicon-s-currency-dollar')
                ->chart([15000, 18000, 22000, 25000, 27000, 30000, 35000])
                ->color('warning'),
        ];
    }
}

// app/Filament/Widgets/RecentEnrollments.php
namespace App\Filament\Widgets;

use App\Models\Enrollment;
use Filament\Tables;
use Filament\Widgets\TableWidget as BaseWidget;
use Illuminate\Database\Eloquent\Builder;

class RecentEnrollments extends BaseWidget
{
    protected static ?int $sort = 3;
    protected int | string | array $columnSpan = 'full';

    protected function getTableQuery(): Builder
    {
        return Enrollment::query()
            ->with(['student.user', 'course'])
            ->latest('enrolled_at')
            ->limit(5);
    }

    protected function getTableColumns(): array
    {
        return [
            Tables\Columns\TextColumn::make('student.user.name')
                ->label('Student')
                ->searchable(),
            Tables\Columns\TextColumn::make('course.title')
                ->label('Course')
                ->searchable(),
            Tables\Columns\TextColumn::make('amount')
                ->money('usd'),
            Tables\Columns\TextColumn::make('enrolled_at')
                ->dateTime()
                ->sortable(),
            Tables\Columns\TextColumn::make('progress')
                ->badge()
                ->color(fn ($state): string => match (true) {
                    $state >= 80 => 'success',
                    $state >= 50 => 'warning',
                    default => 'danger',
                })
                ->suffix('%'),
        ];
    }

    protected function isTablePaginationEnabled(): bool
    {
        return false;
    }

    protected function getHeading(): string
    {
        return 'Recent Enrollments';
    }
}

// app/Filament/Widgets/InstructorPerformance.php
namespace App\Filament\Widgets;

use App\Models\Instructor;
use Filament\Tables;
use Filament\Widgets\TableWidget as BaseWidget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

class InstructorPerformance extends BaseWidget
{
    protected static ?int $sort = 4;
    protected int | string | array $columnSpan = 'full';

    protected function getTableQuery(): Builder
    {
        return Instructor::query()
            ->withCount('courses')
            ->withSum('courses', 'enrollments_count')
            ->withAvg('courses', 'price')
            ->orderByDesc('courses_sum_enrollments_count')
            ->limit(10);
    }

    protected function getTableColumns(): array
    {
        return [
            Tables\Columns\TextColumn::make('user.name')
                ->label('Instructor')
                ->searchable(),
            Tables\Columns\TextColumn::make('courses_count')
                ->label('Total Courses')
                ->sortable(),
            Tables\Columns\TextColumn::make('courses_sum_enrollments_count')
                ->label('Total Enrollments')
                ->sortable(),
            Tables\Columns\TextColumn::make('courses_avg_price')
                ->label('Avg. Course Price')
                ->money('usd')
                ->sortable(),
            Tables\Columns\TextColumn::make('specialization')
                ->searchable(),
        ];
    }

    protected function isTablePaginationEnabled(): bool
    {
        return false;
    }

    protected function getHeading(): string
    {
        return 'Top Performing Instructors';
    }
}

Update the Dashboard to include these widgets:

// app/Filament/Pages/Dashboard.php

class Dashboard extends BaseDashboard
{
    protected function getHeaderWidgets(): array
    {
        return [
            StatsOverview::class,
        ];
    }

    protected function getFooterWidgets(): array
    {
        return [
            RecentEnrollments::class,
            InstructorPerformance::class,
        ];
    }
}

If you made it this far, your admin panel should now have a comprehensive dashboard with key metrics and insights as shown in the image below:

Filament Admin Panel Dashboard

Note:

The dashboard now provides a comprehensive overview with:

  • Key statistics at the top
  • Recent enrollment activity
  • Instructor performance metrics All of this data is automatically updated as new enrollments and courses are added to the system.

Conclusion

That was a lot to go through! If you made it to this point, congratulations! You now have a fully functional professional looking e-learning dashboard. In Part 1 of this tutorial, we’ve built a comprehensive admin panel for an e-learning platform using Laravel Filament. We’ve covered:

  • Setting up a Laravel 11 project with the required packages
  • Building the database structure for an e-learning platform
  • Creating Filament resources for courses, students, and instructors
  • Implementing relationship management between models
  • Adding a powerful dashboard with key metrics and insights
  • Including sample data to showcase the platform’s capabilities

The admin panel we’ve built provides:

  • Easy course and content management
  • Student enrollment tracking
  • Instructor performance monitoring
  • Progress tracking and reporting
  • Rich data visualization

What’s Coming in Part 2

In the second part of this tutorial, we’ll focus on building the student-facing frontend of our platform. You can expect to learn about:

  1. Frontend Development with Volt
  • Building the course catalog
  • Implementing the course details page
  • Creating an intuitive student dashboard
  • Enabling course enrollment and progress tracking
  1. Testing Our Application
  • Writing tests for our resources
  • Testing Volt components

Get Ready for Part 2

To prepare for Part 2:

  1. Ensure your admin panel is working correctly
  2. Run the seeders to have sample data ready
  3. Familiarize yourself with Volt (Livewire 3)
  4. Review the current codebase

The complete source code for Part 1 is available on GitHub. If you find any bugs or run into issues during development, feel free to open an issue or reach out to me on Twitter at @stpopoola.