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
Historyinside aUsersubdirectory:
bash
php artisan livewire:make User/History1
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.phpfile. - Add a new route group for user-specific pages. This group will be protected by the
authmiddleware 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.phpfile. - Inside the
@authblock, find the "Order history" link and update itshrefto point to our newly createduser.historyroute.
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
@endauth1
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.phpand 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 relatedorderlinesin 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
divin 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
coverand add it to the$appendsarray- 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_idexists 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.pngplaceholder image.
protected $appends = ['cover'];: It tells Laravel to automatically append thecoverattribute to everyOrderlinemodel 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 Illuminate\Database\Eloquent\Relations\BelongsTo;
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
33
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
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 itssrcattribute from the static placeholder to the dynamic$orderline->coverattribute.
- 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
divthat holds the orders grid, add an@ifstatement to check if the$orderscollection is empty. - We use the
isEmpty()method to check if the$ordersvariable 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