Appearance
Basket
Preparation
Create a Basket component
- Create a new Livewire Basket component with the terminal command
php artisan livewire:make Basket
- app/Livewire/Basket.php (the component class)
- resources/views/livewire/basket.blade.php (the component view)
- Open the component class and change the layout to
layouts.vinylshop
php
<?php
namespace App\Livewire;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Basket extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render()
{
return view('livewire.basket');
}
}
<?php
namespace App\Livewire;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Basket extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render()
{
return view('livewire.basket');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Add a new route
- Add a new get-route for the Basket to the routes/web.php file
- Update the navigation menu in resources/views/livewire/layout/nav-bar.blade.php
- Add a get-route to the routes/web.php file
- The basket route, with the name shop will be handled by the Basket::class component
- Don't forget to import the Basket component class at the top of the file (
use App\Http\Basket;
)
php
Route::view('/', 'home')->name('home');
Route::get('shop', Shop::class)->name('shop');
Route::view('contact', 'contact')->name('contact');
Route::get('basket', Basket::class)->name('basket');
Route::view('playground', 'playground')->name('playground');
Route::view('under-construction', 'under-construction')->name('under-construction');
Route::get('itunes', Itunes::class)->name('itunes');
Route::middleware(['auth', 'active', 'admin'])->prefix('admin')->name('admin.')->group(function () {
...
});
...
Route::view('/', 'home')->name('home');
Route::get('shop', Shop::class)->name('shop');
Route::view('contact', 'contact')->name('contact');
Route::get('basket', Basket::class)->name('basket');
Route::view('playground', 'playground')->name('playground');
Route::view('under-construction', 'under-construction')->name('under-construction');
Route::get('itunes', Itunes::class)->name('itunes');
Route::middleware(['auth', 'active', 'admin'])->prefix('admin')->name('admin.')->group(function () {
...
});
...
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Create a log component
- Create a new basket-log component inside the components folder:
resources/views/components/tmk/basket-log.blade.php
- Paste the following code inside the component:
basket-log.blade.php code
- Line 6 - 27: this (debug) code is only visible when the
APP_DEBUG
environment variable is set totrue
(not in production) and can be removed later
blade
@props([
'record' => 15 // default value is 15
])
{{-- show this (debug) code only in development APP_ENV=local --}}
@env('local')
<x-tmk.section
x-data="{ show: true }"
@dblclick="show = !show"
class="bg-yellow-50 mt-8 cursor-pointer">
<p class="font-bold">What's inside my basket?</p>
<div x-show="show" x-cloak="">
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getCart():</p>
<pre class="text-sm">@json(Cart::getCart(), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getRecords():</p>
<pre class="text-sm">@json(Cart::getRecords(), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getOneRecord({{$record}}):</p>
<pre class="text-sm">@json(Cart::getOneRecord((int)$record), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p><span class="text-rose-800 font-bold pr-2">Cart::getKeys():</span>@json(Cart::getKeys(), JSON_PRETTY_PRINT)</p>
<p><span class="text-rose-800 font-bold pr-2">Cart::getTotalPrice():</span>@json(Cart::getTotalPrice(), JSON_PRETTY_PRINT)</p>
<p><span class="text-rose-800 font-bold pr-2">Cart::getTotalQty():</span>@json(Cart::getTotalQty(), JSON_PRETTY_PRINT)</p></div>
</x-tmk.section>
@endenv
@props([
'record' => 15 // default value is 15
])
{{-- show this (debug) code only in development APP_ENV=local --}}
@env('local')
<x-tmk.section
x-data="{ show: true }"
@dblclick="show = !show"
class="bg-yellow-50 mt-8 cursor-pointer">
<p class="font-bold">What's inside my basket?</p>
<div x-show="show" x-cloak="">
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getCart():</p>
<pre class="text-sm">@json(Cart::getCart(), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getRecords():</p>
<pre class="text-sm">@json(Cart::getRecords(), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p class="text-rose-800 font-bold">Cart::getOneRecord({{$record}}):</p>
<pre class="text-sm">@json(Cart::getOneRecord((int)$record), JSON_PRETTY_PRINT)</pre>
<hr class="my-4">
<p><span class="text-rose-800 font-bold pr-2">Cart::getKeys():</span>@json(Cart::getKeys(), JSON_PRETTY_PRINT)</p>
<p><span class="text-rose-800 font-bold pr-2">Cart::getTotalPrice():</span>@json(Cart::getTotalPrice(), JSON_PRETTY_PRINT)</p>
<p><span class="text-rose-800 font-bold pr-2">Cart::getTotalQty():</span>@json(Cart::getTotalQty(), JSON_PRETTY_PRINT)</p></div>
</x-tmk.section>
@endenv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Basic scaffolding for view
- Open resources/views/livewire/basket.blade.php and replace the content with the following code:
- The actual content of the view will be added later
php
<div>
<x-tmk.basket-log/>
</div>
<div>
<x-tmk.basket-log/>
</div>
1
2
3
2
3
Add a record to the basket
- Open app/Livewire/Shop.php and resources/views/livewire/shop.blade.php
- Add the "add to basket" logic the component class and the view
- Line 3: add the
wire:click
directive to the Add to basket button - Line 4: remove
<br><span class='text-red-300'>NOT IMPLEMENTED YET</span>
from thedata-tippy-content
attribute
php
@if($record->stock > 0)
<button class="w-6 hover:text-red-900"
wire:click="addToBasket({{ $record->id }})"
data-tippy-content="Add to basket">
<x-phosphor-shopping-bag-light
class="outline-0"
/>
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
@if($record->stock > 0)
<button class="w-6 hover:text-red-900"
wire:click="addToBasket({{ $record->id }})"
data-tippy-content="Add to basket">
<x-phosphor-shopping-bag-light
class="outline-0"
/>
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Update the navigation bar
- Every time the basket is updated, the navigation bar should be updated immediately as well
- The total number of records in the basket should be shown as a badge on the basket-icon in the navigation bar and should be updated automatically
- Open app/Livewire/Layout/NavBar.php and resources/views/livewire/layout/nav-bar.blade.php
- Line 3: add some classes to the basket-link
- Line 6 - 9: add an absolute positioned badge to the basket-link
- Line 5 - 10: the badge is only visible when the total number of records in the basket is greater than 0
php
{{-- shopping cart --}}
<x-nav-link href="{{ route('basket') }}" :active="request()->routeIs('basket')"
class="relative mr-3">
<x-fas-shopping-basket class="w-4 h-4"/>
@if(Cart::getTotalQty() > 0)
<span
class="absolute -top-2 -right-2 text-xs bg-rose-500 text-rose-100 rounded-full w-4 h-4 flex items-center justify-center">
{{ Cart::getTotalQty() }}
</span>
@endif
</x-nav-link>
{{-- shopping cart --}}
<x-nav-link href="{{ route('basket') }}" :active="request()->routeIs('basket')"
class="relative mr-3">
<x-fas-shopping-basket class="w-4 h-4"/>
@if(Cart::getTotalQty() > 0)
<span
class="absolute -top-2 -right-2 text-xs bg-rose-500 text-rose-100 rounded-full w-4 h-4 flex items-center justify-center">
{{ Cart::getTotalQty() }}
</span>
@endif
</x-nav-link>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Update the basket component
- Now we'll make this a 'real' basket where we can:
- Add/remove items from the basket
- Empty our basket
- Place an order and add all items (in the basket) to the database
- The logic inside the basket view is as follows:
- If the cart is empty: show a message (that the cart is empty)
- If the cart is not empty:
- Show all the items in a responsive table
- Provide a link for each item to increase/decrease the number of items
- Show the total price of all items in your basket
- If the user is logged in, show a button to actually place the order
- If the user is not logged in, show a message that he must login/register first
Show a message if the cart is empty
- First, make sure that the cart when the page is loaded
- Line 2: empty the basket when the view is rendered
IMPORTANT: delete this line immediately after you've tested the functionality
- Line 2: empty the basket when the view is rendered
- Line 6 - 8: show a message if the cart is empty
php
<div>
{{ Cart::empty() }}
@if(Cart::getTotalQty() === 0)
{{-- Cart is empty --}}
<x-tmk.alert type="info" class="w-full">
Your basket is empty
</x-tmk.alert>
@else
{{-- Cart is not empty --}}
@endif
<x-tmk.basket-log />
</div>
<div>
{{ Cart::empty() }}
@if(Cart::getTotalQty() === 0)
{{-- Cart is empty --}}
<x-tmk.alert type="info" class="w-full">
Your basket is empty
</x-tmk.alert>
@else
{{-- Cart is not empty --}}
@endif
<x-tmk.basket-log />
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Show all items in the basket
- Add a few records to the basket and check the result in the browser
- Line 3 - 7: empty the basket and emit the
basketUpdated
event - Line 9 - 13: decrease the quantity of the record by one and emit the
basketUpdated
event - Line 15 - 19: increase the quantity of the record by one and emit the
basketUpdated
event - Line 21: refresh the page when the
basketUpdated
event is emitted
php
class Basket extends Component
{
public function emptyBasket()
{
Cart::empty();
$this->dispatch('basketUpdated');
}
public function decreaseQty(Record $record)
{
Cart::delete($record);
$this->dispatch('basketUpdated');
}
public function increaseQty(Record $record)
{
Cart::add($record);
$this->dispatch('basketUpdated');
}
#[On('basketUpdated')]
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render() { ... }
}
class Basket extends Component
{
public function emptyBasket()
{
Cart::empty();
$this->dispatch('basketUpdated');
}
public function decreaseQty(Record $record)
{
Cart::delete($record);
$this->dispatch('basketUpdated');
}
public function increaseQty(Record $record)
{
Cart::add($record);
$this->dispatch('basketUpdated');
}
#[On('basketUpdated')]
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render() { ... }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Check for backorders
- If a user tries to add more records to the basket than there are in stock, the user should be notified about the backorder
- Line 3: add a
$backorder
property and set it to an empty array - Line 14 - 25: the
updateBackorder()
method checks if the quantity of the record is higher than the stock- Line 16: reset the
$backorder
property - Line 18 - 24: loop through all record keys in the cart
- Line 19: get the quantity of the selected record
- Line 20: fetch the selected record from the database
- Line 21: check if the ordered quantity is higher than the stock
- Line 22 - 23: if the quantity is higher than the stock, add the record to the
$backorder
property
- Line 16: reset the
- Line 30: call the
updateBackorder()
method in themount()
method, so it's called every time something changes in the component
php
class Basket extends Component
{
public $backorder = [];
public function emptyBasket() { ... }
public function decreaseQty(Record $record) { ... }
public function increaseQty(Record $record) { ... }
public function updateBackorder()
{
$this->backorder = [];
// loop over records in basket and check if qty > in stock
foreach (Cart::getKeys() as $id) {
$qty = Cart::getOneRecord($id)['qty'];
$record = Record::findOrFail($id);
$shortage = $qty - $record->stock;
if ($shortage > 0)
$this->backorder[] = $shortage . ' x ' . $record->artist . ' - ' . $record->title;
}
}
#[On('basketUpdated')]
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render()
{
$this->updateBackorder();
return view('livewire.basket');
}
}
class Basket extends Component
{
public $backorder = [];
public function emptyBasket() { ... }
public function decreaseQty(Record $record) { ... }
public function increaseQty(Record $record) { ... }
public function updateBackorder()
{
$this->backorder = [];
// loop over records in basket and check if qty > in stock
foreach (Cart::getKeys() as $id) {
$qty = Cart::getOneRecord($id)['qty'];
$record = Record::findOrFail($id);
$shortage = $qty - $record->stock;
if ($shortage > 0)
$this->backorder[] = $shortage . ' x ' . $record->artist . ' - ' . $record->title;
}
}
#[On('basketUpdated')]
#[Layout('layouts.vinylshop', ['title' => 'Your shopping basket', 'description' => 'Your shopping basket',])]
public function render()
{
$this->updateBackorder();
return view('livewire.basket');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Checkout
The checkout is done in three steps:
- The shipping address is entered by the user
- Add the order to the database and update the stock
- Send a confirmation email to the user and to the administrators with the order details, and empty the basket
Create a new Form object for the shipping address
- Create a form object class with the command
php artisan livewire:form ShippingForm
- This will create a new class file
ShippingForm.php
in the folderApp\Livewire\Forms
- Open the file and replace the content with the following code:
php
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ShippingForm extends Form
{
#[Validate('required')]
public $address = null;
#[Validate('required')]
public $city = null;
#[Validate('required|numeric')]
public $zip = null;
#[Validate('required')]
public $country = null;
public $notes = null;
}
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ShippingForm extends Form
{
#[Validate('required')]
public $address = null;
#[Validate('required')]
public $city = null;
#[Validate('required|numeric')]
public $zip = null;
#[Validate('required')]
public $country = null;
public $notes = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Modal for shipping address
- Line 4: add a
$showModal
property for the modal - Line 5: link the
$form
property to theShippingForm
class - Line 7 - 12: the
checkoutForm()
method:- reset all the fields in the
ShippingForm
class and reset the error bag - sets the
$showModal
property totrue
to show the modal
- reset all the fields in the
- Line 14 - 42: the checkout process is called when the PLACE ORDER button in the modal is clicked.
The code is straight forward:- validate the form (= shipping address)
- close the modal
- check for backorders (maybe another user ordered the same record in the meantime)
- add the order to the database, update the stock and send the confirmation email
(these features are implemented in the next steps) - reset the
ShippingForm
, the$backorder
property and the error bag - empty the basket and refresh the component
- show a success message
php
class Basket extends Component
{
public $backorder = [];
public $showModal = false;
public ShippingForm $form;
public function checkoutForm()
{
$this->form->reset();
$this->resetErrorBag();
$this->showModal = true;
}
public function checkout()
{
// validate the form
$this->form->validate();
// hide the modal
$this->showModal = false;
// check if there are records in backorder
$this->updateBackorder();
// add the order to the database
// update the stock
// send confirmation email to the user and to the administrators
// reset the form, backorder array and error bag
$this->form->reset();
$this->reset('backorder');
$this->resetErrorBag();
// empty the cart
Cart::empty();
$this->dispatch('basketUpdated');
// show a confirmation message
$this->dispatch('swal:confirm', [
'icon' => 'success',
'background' => 'success',
'html' => "Thank you for your order.<br>The records will be shipped as soon as possible.",
'showConfirmButton' => false,
'showCancelButton' => false,
]);
}
...
}
class Basket extends Component
{
public $backorder = [];
public $showModal = false;
public ShippingForm $form;
public function checkoutForm()
{
$this->form->reset();
$this->resetErrorBag();
$this->showModal = true;
}
public function checkout()
{
// validate the form
$this->form->validate();
// hide the modal
$this->showModal = false;
// check if there are records in backorder
$this->updateBackorder();
// add the order to the database
// update the stock
// send confirmation email to the user and to the administrators
// reset the form, backorder array and error bag
$this->form->reset();
$this->reset('backorder');
$this->resetErrorBag();
// empty the cart
Cart::empty();
$this->dispatch('basketUpdated');
// show a confirmation message
$this->dispatch('swal:confirm', [
'icon' => 'success',
'background' => 'success',
'html' => "Thank you for your order.<br>The records will be shipped as soon as possible.",
'showConfirmButton' => false,
'showCancelButton' => false,
]);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Add the order to the database
Speedup tips
- Temporary comment out the
Cart::empty()
line in thecheckout()
method, so you don't have to add records over and over again to test the rest of the code - Temporary set fixed values for the
ShippingForm
property through thecheckoutForm()
method, so you don't have to fill in the form over and over again to test the rest of the code
php
public function checkoutForm()
{
$this->form->reset();
$this->resetErrorBag();
$this->showModal = true;
// for debugging only
$this->form->address = 'Kleinhoestraat 4';
$this->form->city = 'Geel';
$this->form->zip = '2440';
$this->form->country = 'Belgium';
$this->form->notes = "Please leave the package at the back door.\nThank you.";
}
public function checkoutForm()
{
$this->form->reset();
$this->resetErrorBag();
$this->showModal = true;
// for debugging only
$this->form->address = 'Kleinhoestraat 4';
$this->form->city = 'Geel';
$this->form->zip = '2440';
$this->form->country = 'Belgium';
$this->form->notes = "Please leave the package at the back door.\nThank you.";
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- Take a look at the database and at the cart:
- First you have to insert the
user_id
and thetotal_price
from the cart session into a new row in the orders table. - Next, retrieve the
order_id
from the just inserted row - Finally, loop over all the records inside the cart and add a new row (with the
order_id
and the necessary columns) to the orderlines table
- First you have to insert the
Why doesn't use the Records table?
As you can see in the Orderlines table, some attributes (artist, title, mb_id) are duplicated from the Records table. Why we do this?
Later in this course, we'll add a page where the user can see all his orders and if we rely on the Records table, we can have some problems:
- The price of the record can change, but the price of the record in the order should stay the same
- The record can be deleted from the Records table, but we still want to show the order
That's the reason why we duplicate the data from the Records table to the Orderlines table.
- In the example below: one order contains 3 different records (= 3 orderlines)
- Line 6 - 9: create a new order and add save it to the
$order
property, so we can easily get theorder_id
later - Line 11 - 19: loop over all the records in the cart and add a new row to the orderlines table
- Line 13:
$order->id
is theorder_id
from the just inserted row in the orders table - Line 14 - 18: the other columns are retrieved from the cart session
- Line 13:
- Line 21 - 23: update the stock of the record
- Line 21: get the record from the database
- Line 22: update the stock value:
- If the quantity in the cart is lower than the stock, subtract the quantity from the stock
- If the quantity in the cart is higher or equal than the stock, set the stock to 0
- Line 23: save the record to the database
php
public function checkout()
{
...
// add the order to the database
// create a new order
$order = Order::create([
'user_id' => auth()->user()->id,
'total_price' => Cart::getTotalPrice(),
]);
// loop over the records in the basket and add them to the orderlines table
foreach (Cart::getRecords() as $record) {
Orderline::create([
'order_id' => $order->id,
'artist' => $record['artist'],
'title' => $record['title'],
'mb_id' => $record['mb_id'],
'total_price' => $record['price'],
'quantity' => $record['qty'],
]);
// update the stock
$updateQty = Record::findOrFail($record['id']);
$updateQty->stock > $record['qty'] ? $updateQty->stock -= $record['qty'] : $updateQty->stock = 0;
$updateQty->save();
}
// send confirmation email to the user and to the administrators
...
}
public function checkout()
{
...
// add the order to the database
// create a new order
$order = Order::create([
'user_id' => auth()->user()->id,
'total_price' => Cart::getTotalPrice(),
]);
// loop over the records in the basket and add them to the orderlines table
foreach (Cart::getRecords() as $record) {
Orderline::create([
'order_id' => $order->id,
'artist' => $record['artist'],
'title' => $record['title'],
'mb_id' => $record['mb_id'],
'total_price' => $record['price'],
'quantity' => $record['qty'],
]);
// update the stock
$updateQty = Record::findOrFail($record['id']);
$updateQty->stock > $record['qty'] ? $updateQty->stock -= $record['qty'] : $updateQty->stock = 0;
$updateQty->save();
}
// send confirmation email to the user and to the administrators
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Email confirmation email
- Create a new
OrderConfirmation
mail class with the commandphp artisan make:mail OrderConfirmation --markdown=emails.order-confirmation
- Add a
sendEmail()
method to theShippingForm
class
- Line 8: send the
backorder
information to thesendEmail()
method in theShippingForm
class
php
public function checkout()
{
...
// add the order to the database
// update the stock
// send confirmation email to the user and to the administrators
$this->form->sendEmail($this->backorder);
// reset the form, backorder array and error bag
...
}
public function checkout()
{
...
// add the order to the database
// update the stock
// send confirmation email to the user and to the administrators
$this->form->sendEmail($this->backorder);
// reset the form, backorder array and error bag
...
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11