# Basket
# Preparation
# Create a Basket component
- Create a new Livewire Basket component with the terminal command
php artisan make:livewire Basket
- app/Http/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
class Basket extends Component { public function render() { return view('livewire.basket') ->layout('layouts.vinylshop', [ 'description' => 'Your shopping basket', 'title' => 'Your shopping basket' ]); } }
Copied!
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 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
routes/web.php
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\Livewire\Basket;
)
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::get('itunes', Itunes::class)->name('itunes'); Route::middleware(['auth', 'active', 'admin'])->prefix('admin')->name('admin.')->group(function () { ... }); ...
Copied!
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# Basic scaffolding for view
- Open resources/views/livewire/basket.blade.php and replace the content with the following code:
livewire/basket.blade.php
result
- Line 3 - 24: this (debug) code is only visible when the
APP_DEBUG
environment variable is set totrue
(not in production) and can be removed later - The actual content of the view will be added later
<div> {{-- 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_encode(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_encode(Cart::getRecords(), JSON_PRETTY_PRINT) }}</pre> <hr class="my-4"> <p class="text-rose-800 font-bold">Cart::getOneRecord(15):</p> <pre class="text-sm">{{ json_encode(Cart::getOneRecord(15), JSON_PRETTY_PRINT) }}</pre> <hr class="my-4"> <p><span class="text-rose-800 font-bold pr-4">Cart::getKeys():</span> {{ json_encode(Cart::getKeys()) }}</p> <p><span class="text-rose-800 font-bold pr-4">Cart::getTotalPrice():</span> {{ json_encode(Cart::getTotalPrice()) }}</p> <p><span class="text-rose-800 font-bold pr-4">Cart::getTotalQty():</span> {{ json_encode(Cart::getTotalQty()) }}</p></div> </x-tmk.section> @endenv </div>
Copied!
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Add a record to the basket
- Open app/Http/Livewire/Shop.php and resources/views/livewire/shop.blade.php
- Add the "add to basket" logic the component class and the view
livewire/shop.blade.php
app/Http/Livewire/Shop.php
result
- Line 3: add the
wire:click
directive to the Add to basket button - Line 4: remove "NOT IMPLEMENTED YET" from the the
data-tippy-content
attribute
<x-phosphor-shopping-bag-light class="outline-0" wire:click="addToCart({{ $record->id }})" data-tippy-content="Add to basket"/>
Copied!
1
2
3
4
2
3
4
# Update the navigation bar
- Every time the basket is updated, the navigation bar should be updated 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/Http/Livewire/Layout/NavBar.php
livewire/layout/nav-bar.blade.php
Livewire/Layout/NavBar.php
result
- Line 2: add some classes to the basket-link
- Line 5 - 7: add an absolute positioned badge to the basket-link
- Line 4 - 8: the badge is when the total number of records in the basket is greater than 0
<x-jet-nav-link href="{{ route('basket') }}" :active="request()->routeIs('basket')" class="relative mr-3 {{ Cart::getTotalQty() > 0 ? 'border-none' : '' }}"> <x-fas-shopping-basket class="w-4 h-4"/> @if(Cart::getTotalQty() > 0) <span class="absolute -top-2 -right-2 text-xs text-white bg-rose-500 text-rose-100 rounded-full w-4 h-4 flex items-center justify-center"> {{ Cart::getTotalQty() }} </span> @endif </x-jet-nav-link>
Copied!
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 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 message if cart is empty
livewire/basket.blade.php
result
- 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
<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 {{-- show this (debug) code only in development APP_ENV=local --}} @env('local') ... @endenv </div>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Show all items in the basket
- Add a few records to the basket and check the result in the browser
Livewire/Basket.php
livewire/basket.blade.php
result (not logged in)
result (logged in)
- Line 3: refresh the component when the
cartUpdated
event is emitted - Line 5 - 6: empty the basket and emit the
cartUpdated
event - Line 11 - 15: decrease the quantity of the record by one and emit the
cartUpdated
event - Line 17 - 21: increase the quantity of the record by one and emit the
cartUpdated
event
class Basket extends Component { protected $listeners = ['cartUpdated' => '$refresh']; public function emptyBasket() { Cart::empty(); $this->emit('cartUpdated'); } public function decreaseQty(Record $record) { Cart::delete($record); $this->emit('cartUpdated'); } public function increaseQty(Record $record) { Cart::add($record); $this->emit('cartUpdated'); } public function render() { ... } }
Copied!
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
Livewire/Basket.php
livewire/basket.blade.php
result
- Line 3: add a
$backorder
property and set it to an empty array - Line 13 - 24: the
updateBackorder()
method checks if the quantity of the record is higher than the stock- Line 15: reset the
$backorder
property - Line 17 - 23: loop through all record keys in the cart
- Line 18: get the quantity of the selected record
- Line 19: fetch the selected record from the database
- Line 20: check if the ordered quantity is higher than the stock
- Line 21 - 22: if the quantity is higher than the stock, add the record to the
$backorder
property
- Line 15: reset the
- Line 28: call the
updateBackorder()
method in themount()
method, so it's called every time something changes in the component
class Basket extends Component { public $backorder = []; protected $listeners = ['cartUpdated' => '$refresh']; 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; } } public function render() { $this->updateBackorder(); return view('livewire.basket') ->layout('layouts.vinylshop', [ 'description' => 'Your shopping basket', 'title' => 'Your shopping basket' ]); } }
Copied!
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
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
# 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
# Modal for shipping address
Livewire/Basket.php
livewire/basket.blade.php
result (form validation)
result (feedback)
- Line 4: add a
$showModal
property for the modal - Line 5 - 11: add a
$shipping
property for the shipping address and set all keys tonull
- Line 14 - 19: add a
$rules
property for the validation rules - Line 22 - 27: add a
$messages
property for the validation messages - Line 39 - 44: the
checkoutForm()
method:- resets the
$shipping
property and the error bag - sets the
$showModal
property totrue
to show the modal
- resets the
- Line 46 - 74: the checkout process is called when the PLACE ORDER button 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
$shipping
property, the$backorder
property and the error bag - empty the basket and refresh the component
- show a success message
class Basket extends Component { public $backorder = []; public $showModal = false; public $shipping = [ 'address' => null, 'city' => null, 'zip' => null, 'country' => null, 'notes' => null, ]; // validation rules protected $rules = [ 'shipping.address' => 'required', 'shipping.city' => 'required', 'shipping.zip' => 'required|numeric', 'shipping.country' => 'required', ]; // validation attributes protected $validationAttributes = [ 'shipping.address' => 'address', 'shipping.city' => 'city', 'shipping.country' => 'country', 'shipping.zip' => 'zip', ]; protected $listeners = ['cartUpdated' => '$refresh']; public function emptyBasket() { ... } public function decreaseQty(Record $record) { ... } public function increaseQty(Record $record) { ... } public function updateBackorder() { ... } public function checkoutForm() { $this->reset('shipping'); $this->resetErrorBag(); $this->showModal = true; } public function checkout() { // validate the form $this->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 shipping array, backorder array and error bag $this->reset('backorder'); $this->resetErrorBag(); // empty the cart Cart::empty(); $this->emit('cartUpdated'); // show a confirmation message $this->dispatchBrowserEvent('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, ]); } public function render() { ... } }
Copied!
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 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
$shipping
property, e.g:
public $shipping = [ 'address' => 'Kleinhoefstraat 4', 'city' => 'Geel', 'zip' => '2440', 'country' => 'Belgium', 'notes' => "Please leave the package at the front door.\nThank you.", ];
Copied!
1
2
3
4
5
6
7
2
3
4
5
6
7
- 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 not 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)
Livewire/Basket.php
result (database)
- Line 7 - 10: create a new order and add save it to the
$order
property, so we can easily get theorder_id
later - Line 12 - 20: loop over all the records in the cart and add a new row to the orderlines table
- Line 14:
$order->id
is theorder_id
from the just inserted row in the orders table - Line 15 - 19: the other columns are retrieved from the cart session
- Line 14:
- Line 22 - 24: update the stock of the record
- Line 22: get the record from the database
- Line 23: 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 24: save the record to the database
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 ... }
Copied!
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
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
# Email confirmation email
- Create a new
OrderConfirmation
mail class with the commandphp artisan make:mail OrderConfirmation --markdown=emails.order-confirmation
Livewire/Basket.php
app/Mail/OrderConfirmation.php
resources/views/emails/order-confirmation.blade.php
result in Mailhog
- Line 5 - 25: construct the email body (some static text, the order details, the shipping details and the backorder details)
- Line 27: get all the administrators from the database
- Line 28 - 30: make a new variable
$template
of theContactMail
mailable class - Line 31 - 33: send the email with:
- Line 31:
$to
is the user who placed the order - Line 32:
$cc
is the list of administrators - Line 33:
$template
is theOrderConfirmation
mailable class
- Line 31:
public function checkout() { ... // send confirmation email to the user and to the administrators $message = '<p>Thank you for your order.<br>The records will be delivered as soon as possible.</p>'; $message .= '<ul>'; foreach (Cart::getRecords() as $record) { $message .= "<li>{$record['qty']} x {$record['artist']} - {$record['title']}</li>"; } $message .= '</ul>'; $message .= "<p>Total price: € " . Cart::getTotalPrice() . "</p>"; $message .= '<p><b>Shipping address:</b><br>'; $message .= $this->shipping['address'] . '<br>'; $message .= $this->shipping['zip'] . ' ' . $this->shipping['city'] . '<br>'; $message .= $this->shipping['country'] . '</p>'; $message .= '<p><b>Notes:</b><br>'; $message .= $this->shipping['notes'] . '</p>'; if (count($this->backorder) > 0) { $message .= '<p><b>Backorder:</b></p>'; $message .= '<ul>'; foreach ($this->backorder as $item) { $message .= "<li>{$item}</li>"; } $message .= '</ul>'; } // Get all admins $admins = User::where('admin', true)->select('name', 'email')->get(); $template = new OrderConfirmation([ 'message' => $message, ]); Mail::to(auth()->user()) ->cc($admins) ->send($template); ... }
Copied!
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
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