Crazy Laravel Livewire made me easy to build my E-Commerce (Admin Panel and API) [Part 2]

Fickreey Hidayat
15 min readDec 16, 2021

--

Hi guys, see with me again !

In this time we will continue our learn about build API and Backend for E-Commerce App like Shopee or Lazada.

There are many steps in this part, we will build admin panel too. To build that work well we need model, controller livewire, define route and then view which is all of that can covering CRUD for User, Product Category, Product, and Transaction. As part 1, i will split to be 2 section to easier. Are you interested ?

Lets rock beibeh !

[Section 1] : Create and Set Up Model

If we see the folder project we will find file User.php in directory app/models, It’s automatically created while we install jetstream. It’s mean we no need to create file model for User, we just need to modify that file to like we want, the one is add the field fillable in array which is has variable name $fillabel in file User.php, like this :

protected $fillable = [
'name',
'username',
'phone',
'roles',
'email',
'password',
];

And then we add function method to relation to transactions model in below before bracket closing class, like this :

public function transactions()
{
return $this->hasMany(Transaction::class, 'users_id', 'id');
}

Next, we will create file model for Product Category, we can use php artisan command in cmd. We can write command php artisan make:model ProductCategory in cmd and enter. It’s will created file model in directory app/models with file name “ProductCategory”. Lets open the file and fill code like this :

protected $fillable = [
'name',
];
public function Products()
{
return $this->hasMany(Product::class, 'categories_id', 'id');
}

And then don’t forget add trait SoftDeletes because we will use that.

use Illuminate\Database\Eloquent\SoftDeletes;

And install in here like this :

use HasFactory, SoftDeletes;

Next we create file model for Product with way as previously. Create file with command in cmd php artisan make:model Product. And then write code like this :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
use HasFactory, SoftDeletes;

protected $fillable = [
'name',
'slug',
'price',
'description',
'categories_id',
'tags',
];

public function galleries()
{
return $this->hasMany(ProductGallery::class, 'products_id', 'id');
}

public function category()
{
return $this->hasOne(ProductCategory::class, 'categories_id', 'id');
}
}

Next write command in cmd php artisan make:model ProductGallery to create file model Product Gallery

And then fill code like this :

<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
class ProductGallery extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'products_id',
'url',
];
public function getUrl($url)
{
return config('app.url') . Storage::url($url);
}
}

And then file model Transaction, Write php artisan make:model Transaction to create and write this code in that file :

<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Transaction extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'users_id',
'address',
'payment',
'total_price',
'shipping_price',
'status',
];
public function user()
{
return $this->belongsTo(User::class, 'users_id', 'id');
}
public function items()
{
return $this->hasMany(TransactionItem::class, 'transactions_id', 'id');
}
}

Last, file model for Item Transaction. As previously write in cmd php artisan make:model TransactionItem and write this code in

<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TransactionItem extends Model
{
use HasFactory;
protected $fillable = [
'users_id',
'products_id',
'transactions_id',
'quantity'
];
public function product()
{
return $this->hasOne(Product::class, 'id', 'products_id');
}
}

[Section 2] : Build CRUD Product Category

Okay we see display of dashboard first, write command php artisan serve to make local server

Open browser and write local server url what is given by last command at url bar, like at image it’s mean localhost:8000

Try to register first, click register

And then we try to login

And it will displayed the dashboard

And then it’s time to build CRUD (Create Read Update Delete) Product Category

[Step 1] : Create Product Category

Lets straight to the point, to build CRUD we should create Controller Livewire first. We can use cmd instantly, write php artisan make:livewire Category to create livewire controller Category.

We can choice “no” if we had given rate lvewire, if haven’t by all means choice “yes”. And then it will created file “Category.php” in directory app/http/Livewire and file “category.blade.php” in directory resources/views/livewire.

Then create file and directory with name “admin/categories.blade.php” in directory “recources/views/”

Fill the file “categories.blade.php” with copied code from “dashboard.blade.php” and modify like we need :

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Categories') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
@livewire('category')
</div>
</div>
</div>
</x-app-layout>

And then define the route, please go to file “web.php” in directory “route”. Write this code for CRUD Product Category like this :

Route::get('/', function () {
return view('welcome');
});
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/categories', function () {
return view('admin.categories');
})->name('categories');;
});

Next, add navigation bar with add this code in file “resources/views/navigation-menu.blade.php”

<x-jet-nav-link href="{{ route('categories') }}" :active="request()->routeIs('categories')">
{{ __('Categories') }}
</x-jet-nav-link>

In my code in line 18 until 20

And then reload the browser, we added one navigation menu “Category”, it’s will displayed like below

Click menu Categories.

Open file “app/Http/Livewire/Category.php” and modify like this :

<?phpnamespace App\Http\Livewire;use Livewire\Component;class Category extends Component
{
public $visibleModalForm = false;
public $name;
public function showModal()
{
$this->visibleModalForm = true;
}
public function hideModal()
{
$this->visibleModalForm = false;
}
public function render()
{
return view('livewire.category');
}
}

Then back to file “resources/views/livewire/category.blade.php” to create button, write like this

<div class="p-6">
<x-jet-button wire:click="showModal">
{{ __('Add Category') }}
</x-jet-button>
</div>

Then make form modal to create product category below that code, so the code will be like this

<div class="p-6">
<x-jet-button wire:click="showModal">
{{ __('Add Category') }}
</x-jet-button>
<!-- Show Modal -->
<x-jet-dialog-modal wire:model="visibleModalForm">
<x-slot name="title">
{{ __('Form Add Category') }}
</x-slot>
<x-slot name="content">
<div class="mt-4">
<x-jet-label for="name" value="{{ __('Category Name') }}" />
<x-jet-input type=" text" class="mt-1 block w-3/4" placeholder="{{ __('Category Name') }}" wire:model.defer="name" />
<x-jet-input-error for="name" class="mt-2" />
</div>
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="hideModal">
{{ __('Cancel') }}
</x-jet-secondary-button>
<x-jet-danger-button class="ml-2" wire:click="create" wire:loading.attr="disabled">
{{ __('Add Category') }}
</x-jet-danger-button>
</x-slot>
</x-jet-dialog-modal>
</div>

It will displayed like this

Click “Add Category” to display form category

Yeah ! It’s like we want. Next write this code in “app/Http/Livewire/Category.php” to make create button working well.

<?phpnamespace App\Http\Livewire;use App\Models\ProductCategory;
use Livewire\Component;
class Category extends Component
{
public $visibleModalForm = false;
public $name;
public function rules()
{
return [
'name' => 'required'
];
}
public function create()
{
$this->validate();
ProductCategory::create($this->modelData());
$this->visibleModalForm = false;
$this->resetFields();
}
public function modelData()
{
return [
'name' => $this->name
];
}
public function resetFields()
{
$this->name = null;
}
public function showModal()
{
$this->visibleModalForm = true;
}
public function hideModal()
{
$this->visibleModalForm = false;
}
public function render()
{
return view('livewire.category');
}
}

Let’s try create something in browser

And then see in Database ! It’s created !

Yeah ! We succeed build create Product Category with modal form !

Come on to next step !

[Step 2] : Read Product Category

To display data where in database to screen we should add code in file “resources/views/livewire/category.blade.php” to be like this :

<div class="p-6">
<div class="flex items-center px-4 py-3 text-right sm:px-6">
<x-jet-button wire:click="showModal">
{{ __('Add Category') }}
</x-jet-button>
</div>
<!-- Datatables -->
<table class="w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Name</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Created at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Updated at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@if ($data->count())
@foreach ($data as $item)
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->name }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->created_at }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->updated_at }}</td>
<td class="px-6 py-4 text-center text-base">
<x-jet-button wire:click="showUpdateModal({{ $item->id }})">
{{ __('Update') }}
</x-jet-button>
<x-jet-danger-button class="ml-2" wire:click="showDeleteModal({{ $item->id }})" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-jet-danger-button>
</td>
</tr>
@endforeach
@else
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap" colspan="4">No Result Found</td>
</tr>
@endif
</tbody>
</table>
<!-- Show Modal -->
<x-jet-dialog-modal wire:model="visibleModalForm">
<x-slot name="title">
{{ __('Form Add Category') }}
</x-slot>
<x-slot name="content">
<div class="mt-4">
<x-jet-label for="name" value="{{ __('Category Name') }}" />
<x-jet-input type=" text" class="mt-1 block w-3/4" placeholder="{{ __('Category Name') }}" wire:model.defer="name" />
<x-jet-input-error for="name" class="mt-2" />
</div>
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="hideModal">
{{ __('Cancel') }}
</x-jet-secondary-button>
<x-jet-danger-button class="ml-2" wire:click="create" wire:loading.attr="disabled">
{{ __('Add Category') }}
</x-jet-danger-button>
</x-slot>
</x-jet-dialog-modal>
</div>

And in “app/Http/Livewire/Category.php” add function method read() like this :

public function read()
{
return ProductCategory::paginate(5);
}

Last modify function method render() to be like this :

public function render()
{
return view('livewire.category', [
'data' => $this->read()
]);
}

Congrats ! Product Category data already to enjoyed :)

Yeah, you right step 2 has finished !

[Step 3] : Update Product Category

To covering Update we need add and modify many function method in file “app/Http/Livewire/Category.php”. Add variable $modelId

public $modelId;

And then add function method showUpdateModal() and loadModel()

public function showUpdateModal($id)
{
$this->resetValidation();
$this->resetFields();
$this->modelId = $id;
$this->visibleModalForm = true;
$this->loadModel();
}
public function loadModel()
{
$data = ProductCategory::findOrFail($this->modelId);
$this->name = $data->name;
}

Then to simplify i will modify function method create() to createOrUpdate() like this :

public function createOrUpdate()
{
$this->validate();
ProductCategory::updateOrCreate(['id' => $this->modelId], $this->modelData());
$this->visibleModalForm = false;
$this->resetFields();
}

And then modify resetFields() and showModal() to be like this :

public function resetFields()
{
$this->modelId = null;
$this->name = null;
}
public function showModal()
{
$this->resetValidation();
$this->resetFields();
$this->visibleModalForm = true;
}

Okay, next step modify the button in file “resources/views/livewire/category.blade.php” to be like this :

@if ($modelId)
<x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
{{ __('Update Category') }}
</x-jet-danger-button>
@else
<x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
{{ __('Add Category') }}
</x-jet-danger-button>
@endif

Time to try in browser !

Click Update

Try update name “Shirt” to be “Shirts” (add “s” in the end)

And then GREAT ! We Succeed build the update

Step 3 have finished but i will repair a little of data table. I will add pagination

Import trait “WithPagination” in file “app/Http/Livewire/Category.php”, like this :

use Livewire\WithPagination;

And then install the trait by write use WithPagination; on top of all method :

use WithPagination;public $visibleModalForm = false;
public $name;
public $modelId;
public function rules()
{
return [
'name' => 'required'
];
}

Next add function method to reset page after paginating, i will use method function mount()

public function mount()
{
$this->resetPage();
}

And last modify data table in file “resources/views/livewire/category.blade.php” to be like this :

<!-- Datatables -->
<div class="flex flex-col">
<div class="my-2 overflow-x-auto sm:mx-6 lg:mx-8">
<div class="py-2 align-middle inline-block w-full sm:px-6 lg:px-8">
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table class="w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Name</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Created at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Updated at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@if ($data->count())
@foreach ($data as $item)
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->name }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->created_at }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->updated_at }}</td>
<td class="px-6 py-4 text-center text-base">
<x-jet-button wire:click="showUpdateModal({{ $item->id }})">
{{ __('Update') }}
</x-jet-button>
<x-jet-danger-button class="ml-2" wire:click="showDeleteModal({{ $item->id }})" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-jet-danger-button>
</td>
</tr>
@endforeach
@else
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap" colspan="4">No Result Found</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
</div>
<br />{{ $data->links() }}

Finish ! Let’s see the result !

And then if we have data more than specified amount it will display like this :

Yeah ! This step is finished.

[Step 4] : Delete Product Category

First, add variable and method function to show modal confirmation delete. We can make variable $confirmDeleteModal with value false in file “app/Http/Livewire/Category.php”

public $confirmDeleteModal = false;

And then add method function to change that value to be true and set up deletion like this

public function showDeleteModal($id)
{
$this->modelId = $id;
$this->confirmDeleteModal = true;
}

Then add this method function for successful deletion

public function delete()
{
ProductCategory::destroy($this->modelId);
$this->confirmDeleteModal = false;
}

Then make dialog modal display in file “resources/views/livewire/category.blade.php”, put this code below dialog modal create code.

<!-- Show Delete Modal -->
<x-jet-dialog-modal wire:model="confirmDeleteModal">
<x-slot name="title">
{{ __('Delete Category') }}
</x-slot>
<x-slot name="content">
{{ __('Are you sure you want to delete this Product Category data ? Once your data is deleted,all of its resources and data will be permanently deleted.') }}
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="$toggle('confirmDeleteModal')">
{{ __('Cancel') }}
</x-jet-secondary-button>
<x-jet-danger-button class="ml-2" wire:click="delete" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-jet-danger-button>
</x-slot>
</x-jet-dialog-modal>

Okay, time to try in browser

Let’s try to delete category with name “Underware”

And then click delete.

Yeah, the data what named “Underware” successfully deleted ! It’s mean this is really successfully work very well !

Before closing i will show you my latest code till now

It’s file “resources/views/livewire/category.blade.php”

<div class="p-6">
<div class="flex items-center px-4 py-3 text-right sm:px-6">
<x-jet-button wire:click="showModal">
{{ __('Add Category') }}
</x-jet-button>
</div>
<!-- Datatables -->
<div class="flex flex-col">
<div class="my-2 overflow-x-auto sm:mx-6 lg:mx-8">
<div class="py-2 align-middle inline-block w-full sm:px-6 lg:px-8">
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table class="w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Name</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Created at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Updated at</th>
<th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@if ($data->count())
@foreach ($data as $item)
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->name }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->created_at }}</td>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->updated_at }}</td>
<td class="px-6 py-4 text-center text-base">
<x-jet-button wire:click="showUpdateModal({{ $item->id }})">
{{ __('Update') }}
</x-jet-button>
<x-jet-danger-button class="ml-2" wire:click="showDeleteModal({{ $item->id }})" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-jet-danger-button>
</td>
</tr>
@endforeach
@else
<tr>
<td class="px-6 py-4 text-center text-base whitespace-no-wrap" colspan="4">No Result Found</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
</div>
<br />{{ $data->links() }}<!-- Show Modal -->
<x-jet-dialog-modal wire:model="visibleModalForm">
<x-slot name="title">
{{ __('Form Add Category') }}
</x-slot>
<x-slot name="content">
<div class="mt-4">
<x-jet-label for="name" value="{{ __('Category Name') }}" />
<x-jet-input type=" text" class="mt-1 block w-3/4" placeholder="{{ __('Category Name') }}" wire:model.defer="name" />
<x-jet-input-error for="name" class="mt-2" />
</div>
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="hideModal">
{{ __('Cancel') }}
</x-jet-secondary-button>
@if ($modelId)
<x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
{{ __('Update Category') }}
</x-jet-danger-button>
@else
<x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
{{ __('Add Category') }}
</x-jet-danger-button>
@endif
</x-slot>
</x-jet-dialog-modal>
<!-- Show Delete Modal -->
<x-jet-dialog-modal wire:model="confirmDeleteModal">
<x-slot name="title">
{{ __('Delete Category') }}
</x-slot>
<x-slot name="content">
{{ __('Are you sure you want to delete this Product Category data ? Once your data is deleted, all of its resources and data will be permanently deleted.') }}
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="$toggle('confirmDeleteModal')">
{{ __('Cancel') }}
</x-jet-secondary-button>
<x-jet-danger-button class="ml-2" wire:click="delete" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-jet-danger-button>
</x-slot>
</x-jet-dialog-modal>
</div>

And then it’s file “app/Http/Livewire/Category.php”

<?phpnamespace App\Http\Livewire;use App\Models\ProductCategory;
use Livewire\Component;
use Livewire\WithPagination;
class Category extends Component
{
use WithPagination;
public $visibleModalForm = false;
public $confirmDeleteModal = false;
public $name;
public $modelId;
public function rules()
{
return [
'name' => 'required'
];
}
public function mount()
{
$this->resetPage();
}
public function read()
{
return ProductCategory::paginate(5);
}
public function createOrUpdate()
{
$this->validate();
ProductCategory::updateOrCreate(['id' => $this->modelId], $this->modelData());
$this->visibleModalForm = false;
$this->resetFields();
}
public function delete()
{
ProductCategory::destroy($this->modelId);
$this->confirmDeleteModal = false;
}
public function showUpdateModal($id)
{
$this->resetValidation();
$this->resetFields();
$this->modelId = $id;
$this->visibleModalForm = true;
$this->loadModel();
}
public function showDeleteModal($id)
{
$this->modelId = $id;
$this->confirmDeleteModal = true;
}
public function loadModel()
{
$data = ProductCategory::findOrFail($this->modelId);
$this->name = $data->name;
}
public function modelData()
{
return [
'name' => $this->name
];
}
public function resetFields()
{
$this->modelId = null;
$this->name = null;
}
public function showModal()
{
$this->resetValidation();
$this->resetFields();
$this->visibleModalForm = true;
}
public function hideModal()
{
$this->visibleModalForm = false;
}
public function render()
{
return view('livewire.category', [
'data' => $this->read()
]);
}
}

Okay, may be enough for this part. Next Part We will do like this again for Product data. Yes, we will build CRUD for Product data.

Thank you for your attention and see you in the next part ! :)

--

--

Fickreey Hidayat

I’m Badminton fans who expertist in PHP Programming specially in Laravel Framework and Pretty Livewire…