Appearance
User: Order History
In this chapter, we'll create a dedicated page where authenticated users can review their past purchases. This "Order History" page will display a list of all orders a user has placed, including details about each item within those orders.
REMARK
Since the order history is primarily a read-only view with no complex user interactions like filtering or sorting (for now), we could have built it using a simple Blade view and a standard controller. However, we've chosen to use a Livewire component. This approach provides a solid foundation, making it much easier to add interactive features, such as re-ordering or detailed views, in the future without a major refactor.
Preparation
Create a Histories Component
First, let's generate the Livewire component that will power our order history page.
- Open your terminal in the project directory.
- Execute the following Artisan command to create a new component named
History
inside aUser
subdirectory:
bash
php artisan livewire:make User/History
1
This command conveniently sets up two files for us:
app/Livewire/User/History.php
: The component class where we'll fetch and manage the order data.resources/views/livewire/user/history.blade.php
: The Blade view that will render the user's order history.
Add en New Route
To make the History
component accessible, we need to define a new route. This route will be protected, ensuring only authenticated users can access their order history.
- Open the
routes/web.php
file. - Add a new route group for user-specific pages. This group will be protected by the
auth
middleware and prefixed with/user
.
php
<?php
use App\Livewire\User\History;
// Other imports
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');
// Other public routes
Route::middleware(['auth', ActiveUser::class])->prefix('user')->group(function () {
Route::redirect('/', '/user/history');
Route::get('history', History::class)->name('user.history');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- Next, open the
resources/views/components/layouts/vinylshop/navbar.blade.php
file. - Inside the
@auth
block, find the "Order history" link and update itshref
to point to our newly createduser.history
route.
php
@auth
<flux:navlist variant="outline">
<flux:navlist.item icon="list-bullet" href="{{ route('user.history') }}">Order history</flux:navlist.item>
</flux:navlist>
{{-- only visible for site administartors --}}
@if (auth()->user()->admin)
{{-- Other admin links --}}
@endif
@endauth
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Basic Scaffolding for the View
Let's create the basic layout for the order history page. We'll design a card-based layout where each card represents a single order.
- Open
resources/views/livewire/user/history.blade.php
and replace its content with the following scaffolding.
This code sets up a responsive grid that will hold our order cards. For now, it contains a single, static example of what an order card will look like, complete with placeholders for the order date, item details, and total price.
After logging in, navigating to the "Order history" page will display our basic, unpopulated card layout.
php
<div>
<x-slot:title>Your Order History</x-slot:title>
<x-slot:description>Your Order History</x-slot:description>
<div class="@container">
<div class="grid items-start grid-cols-1 @2xl:grid-cols-2 @5xl:grid-cols-3 gap-4 mb-4">
<div
class="overflow-hidden rounded border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
<div class="border-b border-zinc-200 px-6 py-4 dark:border-zinc-700">
<flux:heading level="h2">
Order date: ...
</flux:heading>
</div>
<div class="divide-y divide-zinc-200 p-6 dark:divide-zinc-700">
<div class="flex items-center gap-4 py-4 first:pt-0 last:pb-0">
<flux:avatar
src="{{ asset('storage/covers/no-cover.png') }}"
size="xl" alt="Cover"/>
<div class="flex-1">
<flux:heading level="h3">Artist</flux:heading>
<flux:text class="italic">Record Title</flux:text>
<div class="mt-2 flex items-center gap-x-6">
<flux:text size="sm" color="teal">
Quantity: ...
</flux:text>
<flux:text size="sm" color="teal">
Price: ...
</flux:text>
</div>
</div>
</div>
</div>
<div class="border-t border-zinc-200 bg-zinc-50 px-6 py-4 dark:border-zinc-700 dark:bg-zinc-800/50">
<div class="flex items-center justify-end">
<flux:heading level="h5">
Total price: ...
</flux:heading>
</div>
</div>
</div>
</div>
</div>
<x-itf.livewire-log/>
</div>
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
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
Orders Overview
With the structure in place, the core task is to fetch the user's order data from the database and display it dynamically in the view.
- Livewire/User/History.php
- In the
render()
method, we build a query to retrieve the orders. Order::where('user_id', auth()->user()->id)
: It ensures we only fetch orders belonging to the currently authenticated user.->with('orderlines')
: We use eager loading to fetch all relatedorderlines
in a single, efficient query, avoiding the N+1 problem.->orderByDesc('created_at')
: The orders are sorted so the most recent ones appear first.- Finally, the collection of orders is passed to the view using
compact('orders')
.
- In the
- resources/views/livewire/user/history.blade.php
- We wrap our card
div
in an@foreach($orders as $order)
loop. This will create one card for each order fetched from the database. - Inside the card, we display the order's creation date and total price.
- A nested
@foreach($order->orderlines as $orderline)
loop iterates through the items of that specific order. - For each
orderline
, we display the artist, title, quantity, and price. - The
:orders="$orders"
is added to the debug component<x-itf.livewire-log>
to help visualize the data being passed to the view.
- We wrap our card
php
<?php
namespace App\Livewire\User;
use App\Models\Order;
use Livewire\Component;
class History extends Component
{
public function render()
{
$orders = Order::where('user_id', auth()->user()->id)
->with('orderlines')
->orderByDesc('created_at')
->get();
return view('livewire.user.history', compact('orders')); //
}
}
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
Get the Cover Image
The order history is almost complete, but we're missing the album cover for each item. Since we stored the mb_id
in the orderlines
table, we can use it to dynamically construct the path to the cover image. We'll achieve this by adding a new appended attribute to the Orderline
model.
- Open the app/Models/Orderline.php model and make a new attribute
cover
and add it to the$appends
array- We define a new accessor method named
cover()
. This method uses anAttribute::make()
closure to generate its value. - The
get:
closure checks if a cover image corresponding to theorderline
'smb_id
exists in our public storage.- If it exists, the method returns the public URL to that image.
- If not, it gracefully falls back to returning the URL of the
no-cover.png
placeholder image.
protected $appends = ['cover'];
: It tells Laravel to automatically append thecover
attribute to everyOrderline
model instance when it's converted to an array or JSON, making it readily available in our view.
- We define a new accessor method named
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Storage;
class Orderline extends Model
{
// Attributes that are not mass assignable
protected $guarded = ['id', 'created_at', 'updated_at'];
// Relationship between models
public function order()
{
return $this->belongsTo(Order::class)->withDefault(); // an orderline belongs to an "order"
}
protected function cover(): Attribute
{
return Attribute::make(
get: function ($value, $attributes) {
return Storage::disk('public')->exists('covers/' . $attributes['mb_id'] . '.jpg')
? Storage::url('covers/' . $attributes['mb_id'] . '.jpg')
: Storage::url('covers/no-cover.png');
},
);
}
protected $appends = ['cover'];
}
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
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
Now, we just need to use this new attribute in our view.
- resources/views/livewire/user/history.blade.php file.
- Locate the
<flux:avatar>
component (which renders the<img>
tag) and update itssrc
attribute from the static placeholder to the dynamic$orderline->cover
attribute.
- Locate the
php
<flux:avatar
src="{{ $orderline->cover }}"
size="xl" alt="Cover"/>
1
2
3
2
3
The final result is a complete and visually appealing order history page, with all data, including cover images, dynamically loaded for each user.
No Orders Yet
It's important to handle the edge case where a user has not yet placed any orders. Instead of showing an empty page, we should display a friendly message. We can easily achieve this with a simple conditional statement in our view.
- resources/views/livewire/user/history.blade.php file:
- Before the main container
div
that holds the orders grid, add an@if
statement to check if the$orders
collection is empty. - We use the
isEmpty()
method to check if the$orders
variable contains any items.- If it's empty, we display an informative alert to the user.
- The existing grid of orders will only be rendered if the collection is not empty.
- Before the main container
php
<div>
<x-slot:title>Your Order History</x-slot:title>
<x-slot:description>Your Order History</x-slot:description>
@if($orders->isEmpty())
<x-itf.alert variant="info" class="mb-4">
You don't have any orders yet.
</x-itf.alert>
@endif
<div class="@container"> ... </div>
<x-itf.livewire-log :orders="$orders"/>
</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