Develop Laravel e-commerce website step by step
Step 1: Create the Laravel Project
Create the Laravel Project: Open your terminal and run the following command:
composer create-project laravel/laravel ecommerce-admin
cd ecommerce-admin
Open Project in VS Code
code .
Install Dependencies for Jetstream:
composer require laravel/jetstream
Install Jetstream with Inertia (since it works seamlessly with Bootstrap 5):
php artisan jetstream:install livewire
Install npm dependencies and build assets:
npm install
npm run dev
Run the Migrations:
php artisan migrate
Step 2: Configure Multi-User Authentication (Admin & Manager Roles)
Update the User model to support roles: Open the app/Models/User.php file and add the following code to manage roles:
public function hasRole($role)
{
return $this->role === $role;
}
Add the role field to the users table: Update the migration for the users table. Open database/migrations/xxxx_xx_xx_create_users_table.php:
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role')->default('manager'); // Default role is 'manager'
$table->rememberToken();
$table->timestamps();
});
}
Run the Migration:
php artisan migrate
Assign Roles to Users: Seed admin and manager users. Open database/seeders/DatabaseSeeder.php
and add:
use App\Models\User;
use Illuminate\Support\Facades\Hash;
public function run()
{
User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password123'),
'role' => 'admin',
]);
User::create([
'name' => 'Manager User',
'email' => 'manager@example.com',
'password' => Hash::make('password123'),
'role' => 'manager',
]);
}
Run the Seeder:
php artisan db:seed
Step 3: Implement Middleware for Role-Based Access Control
Create a Custom Middleware:
Run the following command to create the middleware:
php artisan make:middleware RoleMiddleware
Update the Middleware: Open the newly created middleware at app/Http/Middleware/RoleMiddleware.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next, string $role): Response
{
// Ensure the user is authenticated
if (!$request->user()) {
return redirect()->route('login')->with('error', 'Please log in to access this page.');
}
// Ensure the user has the required role
if ($request->user()->role !== $role) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}
Explanation
Role Checking:
The middleware checks if the authenticated user has the specified role. If not, it returns a 403 Unauthorized response.
Handling Missing Users:
It ensures the user is authenticated before checking the role by using $request->user(). If the user is not logged in or doesn’t have the required role, the request is aborted.
Register the Middleware in bootstrap/app.php
Open bootstrap/app.php and register the middleware alias inside the middleware configuration.
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Foundation\Configuration\Exceptions;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
]);
// Register custom middleware alias
$middleware->alias([
'role' => \App\Http\Middleware\RoleMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Create the necessary controllers
php artisan make:controller AdminController
php artisan make:controller ManagerController
Define Controller Methods
AdminController.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AdminController extends Controller
{
public function dashboard()
{
return view('admin.dashboard');
}
public function orders()
{
return view('admin.orders');
}
}
ManagerController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ManagerController extends Controller
{
public function orders()
{
return view('manager.orders');
}
}
Update Routes in web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\ManagerController;
Route::get('/', function () {
return view('welcome');
});
// Middleware group for authenticated users
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
// General dashboard for all authenticated users
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
// Admin-specific routes
Route::middleware('role:admin')->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])->name('admin.dashboard');
Route::get('/admin/orders', [AdminController::class, 'orders'])->name('admin.orders');
});
// Manager-specific routes
Route::middleware('role:manager')->group(function () {
Route::get('/manager/orders', [ManagerController::class, 'orders'])->name('manager.orders');
});
});
Create Views for Admin and Manager
Create the views in resources/views/admin/ and resources/views/manager/
resources/views/admin/dashboard.blade.php
<h1>Admin Dashboard</h1>
<p>Welcome to the admin panel!</p>
resources/views/admin/orders.blade.php
<h1>Manage Orders (admin)</h1>
<p>Here are all the orders for the admin to manage.</p>
resources/views/manager/orders.blade.php
<h1>Manager Orders (Manager)</h1>
<p>Here are the orders for the manager to handle.</p>
Create Product Category CRUD operation with AJAX, Laravel, and Bootstrap 5, along with SweetAlert for alerts.
Create Migration and Model
php artisan make:model Category -m
Migration File for Categories
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCategoriesTable extends Migration
{
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('categories');
}
}
Run the Migration
php artisan migrate
Set fillable to Category model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['name', 'description'];
}
Create Controller for Categories
php artisan make:controller CategoryController --resource
Controller Logic: CategoryController.php
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
use Response;
class CategoryController extends Controller
{
public function index()
{
if (request()->ajax()) {
$categories = Category::all();
return response()->json($categories);
}
return view('admin.categories.index');
}
public function show($id)
{
$category = Category::findOrFail($id);
return response()->json($category);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|unique:categories|max:255',
'description' => 'nullable|string',
]);
Category::create($validated);
return response()->json(['success' => 'Category added successfully.']);
}
public function update(Request $request, $id)
{
$validated = $request->validate([
'name' => 'required|max:255|unique:categories,name,' . $id,
'description' => 'nullable|string',
]);
$category = Category::findOrFail($id);
$category->update($validated);
return response()->json(['success' => 'Category updated successfully.']);
}
public function destroy($id)
{
Category::findOrFail($id)->delete();
return response()->json(['success' => 'Category deleted successfully.']);
}
}
Define Routes
use App\Http\Controllers\CategoryController;
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
Route::resource('categories', CategoryController::class)->except(['create', 'edit']);
});
Sample Layout
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'E-commerce Admin')</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
@yield('styles')
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Admin Panel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ route('dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('categories.index') }}">Categories</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="#" onclick="event.preventDefault();
document.getElementById('logout-form').submit();">Logout</a>
</li>
</ul>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</div>
</nav>
<div class="container mt-4">
@yield('content')
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap JS and dependencies -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@yield('scripts')
</body>
</html>
Create Views and AJAX Logic
Create the Blade template for managing categories at resources/views/admin/categories/index.blade.php
index.blade.php
@extends('layouts.ecomapp')
@section('title', 'Manage Categories')
@section('content')
<div class="container">
<h2 class="mt-4">Manage Categories</h2>
<button id="addCategory" class="btn btn-primary mb-3">Add Category</button>
<table class="table table-bordered" id="categoriesTable">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- Modal -->
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="categoryForm">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="categoryId">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$(document).ready(function () {
fetchCategories();
function fetchCategories() {
$.ajax({
url: "{{ route('categories.index') }}",
method: 'GET',
success: function (data) {
let rows = '';
data.forEach(category => {
rows += `<tr>
<td>${category.name}</td>
<td>${category.description}</td>
<td>
<button class="btn btn-sm btn-warning edit" data-id="${category.id}">Edit</button>
<button class="btn btn-sm btn-danger delete" data-id="${category.id}">Delete</button>
</td>
</tr>`;
});
$('#categoriesTable tbody').html(rows);
}
});
}
$('#addCategory').click(function () {
$('#categoryForm')[0].reset(); // Clear all form inputs
$('#categoryId').val(''); // Ensure the hidden ID field is empty
$('#modalTitle').text('Add Category');
$('#categoryModal').modal('show');
});
$('#categoryForm').submit(function (e) {
e.preventDefault();
const id = $('#categoryId').val().trim(); // Trim to avoid whitespaces
const url = id ? `/categories/${id}` : "{{ route('categories.store') }}"; // Determine URL
const method = id ? 'PUT' : 'POST'; // Determine HTTP method
$.ajax({
url: url,
method: 'POST', // Always use POST and emulate PUT if needed
data: {
_method: method, // Emulate PUT if updating
name: $('#name').val(),
description: $('#description').val(),
_token: "{{ csrf_token() }}"
},
success: function (response) {
$('#categoryModal').modal('hide');
Swal.fire('Success', response.success, 'success');
fetchCategories(); // Refresh the categories list
},
error: function (xhr) {
console.error(xhr.responseText); // Log error for debugging
Swal.fire('Error', 'Something went wrong!', 'error');
}
});
});
$(document).on('click', '.edit', function () {
const id = $(this).data('id');
$.get(`/categories/${id}`, function (category) {
$('#categoryId').val(category.id);
$('#name').val(category.name);
$('#description').val(category.description);
$('#modalTitle').text('Edit Category');
$('#categoryModal').modal('show');
});
});
$(document).on('click', '.delete', function () {
const id = $(this).data('id');
Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it!'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: `/categories/${id}`,
method: 'DELETE',
data: {
_token: "{{ csrf_token() }}"
},
success: function (response) {
Swal.fire('Deleted!', response.success, 'success');
fetchCategories();
}
});
}
});
});
});
</script>
@endsection
Subcategory Management
Create Migration and Model for Subcategory
php artisan make:model Subcategory -m
Migration: create_subcategories_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSubcategoriesTable extends Migration
{
public function up()
{
Schema::create('subcategories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->foreignId('category_id')->constrained('categories')->onDelete('cascade');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('subcategories');
}
}
Run the migration:
php artisan migrate
Define Models
Subcategory.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subcategory extends Model
{
use HasFactory;
protected $fillable = ['name', 'description', 'category_id'];
public function category()
{
return $this->belongsTo(Category::class);
}
}
Category.php
Add a relation in your Category model:
public function subcategories()
{
return $this->hasMany(Subcategory::class);
}
Create Controller for Subcategory
php artisan make:controller SubcategoryController --resource
Update SubcategoryController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Subcategory;
use Illuminate\Http\Request;
class SubcategoryController extends Controller
{
public function index($categoryId)
{
$category = Category::findOrFail($categoryId);
$subcategories = $category->subcategories;
return view('admin.subcategories.index', compact('category', 'subcategories'));
}
public function show($id)
{
$subcategory = Subcategory::findOrFail($id);
return response()->json($subcategory);
}
public function store(Request $request, $categoryId)
{
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable|string',
]);
Subcategory::create(array_merge($validated, ['category_id' => $categoryId]));
return redirect()->route('subcategories.index', $categoryId)
->with('success', 'Subcategory added successfully.');
}
public function update(Request $request, $id)
{
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable|string',
]);
$subcategory = Subcategory::findOrFail($id);
$subcategory->update($validated);
return response()->json(['success' => 'Subcategory updated successfully.']);
}
public function destroy($id)
{
Subcategory::findOrFail($id)->delete();
return redirect()->back()->with('success', 'Subcategory deleted successfully.');
}
}
Define Routes
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
Route::resource('categories', CategoryController::class)->except(['create', 'edit']);
Route::get('categories/{category}/subcategories', [SubcategoryController::class, 'index'])->name('subcategories.index');
Route::post('categories/{category}/subcategories', [SubcategoryController::class, 'store'])->name('subcategories.store');
Route::put('subcategories/{subcategory}', [SubcategoryController::class, 'update'])->name('subcategories.update');
Route::delete('subcategories/{subcategory}', [SubcategoryController::class, 'destroy'])->name('subcategories.destroy');
Route::get('subcategories/{subcategory}', [SubcategoryController::class, 'show'])->name('subcategories.show');
});
Update the Category view to add a button
<a href="/categories/${category.id}/subcategories" class="btn btn-sm btn-info">
Manage Subcategories
</a>
Create Views for Subcategories
Create the index view for subcategories at resources/views/admin/subcategories/index.blade.php:
@extends('layouts.ecomapp')
@section('title', 'Manage Subcategories')
@section('content')
<div class="container">
<h2 class="mt-4">Manage Subcategories for {{ $category->name }}</h2>
<button id="addSubcategory" class="btn btn-primary mb-3">Add Subcategory</button>
<table class="table table-bordered" id="subcategoriesTable">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($subcategories as $subcategory)
<tr>
<td>{{ $subcategory->name }}</td>
<td>{{ $subcategory->description }}</td>
<td>
<button class="btn btn-warning btn-sm edit" data-id="{{ $subcategory->id }}">Edit</button>
<form action="{{ route('subcategories.destroy', $subcategory->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Add/Edit Subcategory Modal -->
<div class="modal fade" id="subcategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="subcategoryForm">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="subcategoryId">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$(document).on('click', '#addSubcategory', function () {
$('#subcategoryForm')[0].reset(); // Clear the form
$('#subcategoryId').val(''); // Clear hidden ID field
$('#modalTitle').text('Add Subcategory');
$('#subcategoryModal').modal('show');
});
$('#subcategoryForm').submit(function (e) {
e.preventDefault();
const id = $('#subcategoryId').val();
const url = id ? `/subcategories/${id}` : "{{ route('subcategories.store', $category->id) }}";
const method = id ? 'PUT' : 'POST';
$.ajax({
url: url,
method: 'POST', // Always use POST and emulate PUT if needed
data: {
_method: method, // Emulate the correct HTTP method
name: $('#name').val(),
description: $('#description').val(),
_token: "{{ csrf_token() }}"
},
success: function (response) {
$('#subcategoryModal').modal('hide'); // Hide modal on success
location.reload(); // Reload the page to show the new data
},
error: function (xhr) {
console.error(xhr.responseText); // Log errors for debugging
Swal.fire('Error', 'Failed to save the subcategory!', 'error');
}
});
});
$(document).on('click', '.edit', function () {
const id = $(this).data('id');
$.ajax({
url: `/subcategories/${id}`, // Fetch the subcategory details
method: 'GET',
success: function (subcategory) {
// Populate the modal with the subcategory data
$('#subcategoryId').val(subcategory.id);
$('#name').val(subcategory.name);
$('#description').val(subcategory.description);
$('#modalTitle').text('Edit Subcategory');
$('#subcategoryModal').modal('show');
},
error: function (xhr) {
console.error(xhr.responseText);
Swal.fire('Error', 'Failed to load subcategory details!', 'error');
}
});
});
$(document).on('submit', 'form[action*="subcategories/"]:not(#subcategoryForm)', function (e) {
e.preventDefault();
const form = $(this);
Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it!'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: form.attr('action'),
method: 'POST', // Use POST to delete (with DELETE emulation)
data: {
_method: 'DELETE',
_token: "{{ csrf_token() }}"
},
success: function (response) {
Swal.fire('Deleted!', response.success, 'success');
location.reload(); // Reload to reflect changes
},
error: function (xhr) {
console.error(xhr.responseText);
Swal.fire('Error', 'Failed to delete the subcategory!', 'error');
}
});
}
});
});
</script>
@endsection
Setting up Admin Template for Application
Download the Template using this link
Download Now