# Admin: covers (part 1)
- In this chapter, we can edit the covers of a particular record
- Delete the cover
- Upload a new cover
- Reset the cover by downloading it from the MusicBrainz API
- In the next chapter, we list all the redundant covers in our storage and delete them
WARNING
Even though you can perfectly separate the two functionalities on two different pages, we will keep them on the same page to demonstrate how route-parameters works and how to use those parameters in the Livewire component
# Preparation
# Create a Covers component
- Create a new Livewire component with the terminal command
php artisan make:livewire Admin/Covers
- app/Http/Livewire/Admin/Covers.php (the component class)
- resources/views/livewire/admin/covers.blade.php (the component view)
- Open the component class and change the layout to
layouts.vinylshop
(leave the title empty)
class Covers extends Component { public function render() { return view('livewire.admin.covers') ->layout('layouts.vinylshop', [ 'description' => 'Manage album covers', 'title' => '', ]); } }
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 Covers class to the routes/web.php file
- Update the navigation menu in resources/views/components/layouts/nav-bar.blade.php
routes/web.php
resources/views/livewire/layout/nav-bar.blade.php
Route::view('/', 'home')->name('home'); Route::get('shop', Shop::class)->name('shop'); Route::view('contact', 'contact')->name('contact'); Route::view('playground', 'playground')->name('playground'); Route::get('itunes', Itunes::class)->name('itunes'); Route::middleware(['auth', 'active', 'admin'])->prefix('admin')->name('admin.')->group(function () { Route::redirect('/', '/admin/records'); Route::get('genres', Genres::class)->name('genres'); Route::get('records_old', [RecordController::class, 'index'])->name('records.old'); Route::get('records', Records::class)->name('records'); Route::get('users/basic', UsersBasic::class)->name('users.basic'); Route::get('users/advanced', UsersAdvanced::class)->name('users.advanced'); Route::get('users/expert', UsersExpert::class)->name('users.expert'); Route::get('covers/{id?}', Covers::class)->name('covers'); }); ...
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ROUTE-PARAMETER
- In our route, we have a variable
id
that acts as a route-parameter{id?}
(see: route-parameters (opens new window)) - The
?
means that the parameter is optional - This means that the route can be called with or without a parameter, e.g.:
http://vinyl_shop.test/admin/covers/12
(with parameter)http://vinyl_shop.test/admin/covers
(without parameter)
- You can name the parameter whatever you want, but keep in mind that you have to use the same name in the Livewire component otherwise the parameter will not be passed
# Basic scaffolding for the view
- Open the resources/views/livewire/admin/covers.blade.php file
- Replace the content of file with the following code:
livewire/admin/covers.blade.php
Livewire/Admin/Covers.php
result
<div> @if($record) <h1 class="text-3xl mb-4">Edit cover</h1> <div x-data="" class="w-60 h-60 flex flex-col gap-4"> <img class="border border-gray-300 object-cover rounded shadow-lg" src="/storage/covers/no-cover.png" alt=""> <div class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> <a href="#" class="hover:text-white hover:bg-green-800 border-r border-gray-500">RESET</a> <a href="#" class="hover:text-white hover:bg-red-800">DELETE</a> </div> </div> {{-- Upload cover modal --}} <x-jet-dialog-modal wire:model="showModal"> <x-slot name="title"> <h2 class="text-lg font-bold">Edit cover</h2> </x-slot> <x-slot name="content"> <div class="flex flex-col gap-4"> <div class="flex gap-4"> <img class="w-36 h-36 border border-gray-300 object-cover" id="coverPreview" src="/storage/covers/no-cover.png" alt=""> <div class="flex-1 py-2"> <p class="text-lg font-bold">...</p> <p class="text-sm">....</p> <input type="file" id="cover" wire:model="newCover" wire:loading.attr="disabled" wire:target="newCover" accept="image/*" class="mt-4 file:border-0 file:text-white file:bg-sky-800 file:p-2 file:rounded file:cursor-pointer"> <x-jet-input-error for="newCover" class="mt-2"/> <p class="w-full italic text-sky-700 pt-4" wire:loading wire:target="newCover"> Processing image... </p> </div> </div> </div> </x-slot> <x-slot name="footer"> <x-jet-secondary-button wire:click="$toggle('showModal')" wire:loading.attr="disabled"> Cancel </x-jet-secondary-button> @if($newCover) <x-jet-button class="ml-2" wire:click="saveCover()" wire:loading.attr="disabled">Save </x-jet-button> @endif </x-slot> </x-jet-dialog-modal> @else <h1 class="text-3xl mb-4">Redundant covers</h1> @endif </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
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
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
TIPS
- Don't worry about deleting some covers that belong to a record. You can always get them back from the URL http://vinyl_shop.test/download_covers.php (opens new window) (See: chapter Record covers)
- Refresh the database with the command
php artisan migrate:fresh
- (Optional) add one or more records without a cover to the database, e.g
mb_id
:a36348b3-1950-49fe-b895-49f586afc895
(Daan - Le Franc Belge)ff6284f3-d5f3-4a13-b839-d64a468aa430
(Lidia Lunch - Queen of Siam)58dcd354-a89a-48ea-9e6e-e258cb23e11d
(Ramones - End of the Century)794c6bf2-3241-416f-9b8f-24e2d84a1c4b
(The Stooges - Fun House)
# Add a link on the records page
- Open the file
resources/views/livewire/admin/records.blade.php
livewire/admin/records.blade.php
result
- Wrap the cover image in an anchor tag
- add the attribute
data-tippy-content
with the valueEdit cover
- add the attribute
href
with the valueroute('admin.covers', $record->id)
($record->id
is the parameter that will be passed to theCovers
component)
- add the attribute
<td> <a data-tippy-content="Edit cover" href="{{ route('admin.covers', $record->id) }}"> <img src="{{ $record->cover['url'] }}" alt="{{ $record->title }} by {{ $record->artist }}" class="my-2 border object-cover"> </a> </td>
Copied!
1
2
3
4
5
6
7
2
3
4
5
6
7
# Get the selected record
- The
mount()
method of theCovers
component has a parameter$id
with a default value ofnull
- If the parameter is not
null
, themount()
method will get the record with the given id from the database - If the parameter is
null
, themount()
method don't do anything for now (see part 2)
- If the parameter is not
livewire/admin/covers.blade.php
result ($id not null)
result ($id is null)
public function mount($id = null) { if ($id) { // get the selected record if id is not null $this->record = Record::findOrFail($id); } else { // get all the redundant covers from the disk } }
Copied!
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
`find()` vs `findOrFail()`
- To select one single record from the database, you can use the
findOrFail()
method or thefind()
method - If you try to select a record that doesn't exist:
- the
findOrFail()
method will throw a 404 exception - the
find()
method will returnnull
- the
- Go to the URL http://vinyl_shop.test/admin/covers/999 (opens new window) and look at different results with
find()
andfindOrFail()
# Upload a new cover
# Open the modal
livewire/admin/covers.blade.php
result
- Line 5: replace the dummy cover with the cover of the selected record
- Line 9: add a
wire:click.prevent
attribute to theEDIT
link- the
wire:click.prevent
attribute will call theopenModal()
method of theCovers
component - the
prevent
modifier will prevent the default action of the link (e.g. following thehref
attribute)
- the
- Line 22: replace the dummy cover in the modal with the cover of the selected record
- Line 25 - 26: add the record title and the artist to the modal
- Line 38: the
wire:click
attribute will call thesaveCover()
method and store the new cover to the disk
@if($record) <h1 class="text-3xl mb-4">Edit cover</h1> <div x-data="" class="w-60 h-60 flex flex-col gap-4"> <img class="border border-gray-300 object-cover rounded shadow-lg" src="{{ $record->cover['url'] }}" alt=""> <div class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" wire:click.prevent="openModal()" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> ... </div> </div> {{-- Upload cover modal --}} <x-jet-dialog-modal wire:model="showModal"> ... <x-slot name="content"> <div class="flex flex-col gap-4"> <div class="flex gap-4"> <img class="w-36 h-36 border border-gray-300 object-cover" id="coverPreview" src="{{ $record->cover['url'] }}" alt=""> <div class="flex-1 py-2 relative"> <p class="text-lg font-bold">{{ $record->title }}</p> <p class="text-sm">{{ $record->artist }}</p> ... </div> </div> </div> </x-slot> <x-slot name="footer"> <x-jet-secondary-button wire:click="$toggle('showModal')" wire:loading.attr="disabled"> Cancel </x-jet-secondary-button> @if($newCover) <x-jet-button class="ml-2" wire:click="saveCover()" wire:loading.attr="disabled">Save </x-jet-button> @endif </x-slot> </x-jet-dialog-modal> @else <h1 class="text-3xl mb-4">Redundant covers</h1> @endif
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
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
# Upload a new cover
- Livewire makes it super easy to upload files (opens new window) (not only images) and even shows a preview of the file
- First add the
WithFileUploads
trait to theCovers
component - Now you can use the
wire:model
attribute to bind thenewCover
property to theinput
element with thetype="file"
attribute - The uploaded file has a random name and will be stored in the
storage/app/livewire-temp
directory - You can now access the temporary file with
$newCover->temporaryUrl()
livewire/admin/covers.blade.php
Livewire/Admin/Covers.php
result
- Line 7: is the
newCover
property contains a file, show a preview of the file, else show the original cover
<x-jet-dialog-modal wire:model="showModal"> ... <x-slot name="content"> <div class="flex flex-col gap-4"> <div class="flex gap-4"> <img class="w-36 h-36 border border-gray-300 object-cover" id="coverPreview" src="{{ isset($newCover) ? $newCover->temporaryUrl() : $record->cover['url'] }}" alt=""> <div class="flex-1 py-2 relative"> ... </div> </div> </div> </x-slot> ... </x-jet-dialog-modal>
Copied!
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
WARNING
- Every time you upload a new file, a new temporary file will be created and stored in the
storage/app/livewire-temp
directory - In the screenshot below you can see that I temporarily uploaded 4 files, and they are all stored on the server, in the
storage/app/livewire-temp
directory - Livewire will automatically delete the temporary files after 24 hours, but you can also delete them manually
# Save the cover
- The SAVE button will only be visible if the
newCover
property contains a value
Livewire/Admin/Covers.php
result
- When the SAVE button is clicked, the
saveCover()
method will be called and:- Line 8 - 10: validate the
newCover
property - Line 11: create a new
Image
object from thenewCover
property, resize it to 250x250 pixels and encode it to ajpg
file with a quality of 75% - Line 12: call the
saveToDisk()
method to save the cover to the disk - Line 14 -17: delete all temporary files from the
livewire-tmp
directory (optional) - Line 18: close the modal
- Line 8 - 10: validate the
- The
saveToDisk()
method will;- Line 30: set the path where the cover will be saved
- Line 31: save the cover to the disk
- Line 32: reset the
newCover
andshowModal
properties
class Covers extends Component { ... // get the uploaded cover and save it to the disk public function saveCover() { $this->validate([ 'newCover' => 'required|image|mimes:jpg,jpeg,png,webp', ]); $cover = Image::make($this->newCover->getRealPath())->fit(250, 250)->encode('jpg', 75); $this->saveToDisk($cover); // delete all temporary files from the livewire-tmp directory (optional) $files = Storage::disk('local')->files('livewire-tmp'); foreach ($files as $file) { Storage::disk('local')->delete($file); } $this->showModal = false; } // try to get the original cover from the coverartarchive.org public function getOriginalCover() {} // delete the cover from the disk public function deleteCover() {} // save the cover to the disk public function saveToDisk($cover) { $coverName = 'covers/' . $this->record->mb_id . '.jpg'; Storage::disk('public')->put($coverName, $cover, 'public'); $this->reset('newCover', 'showModal'); } ... }
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
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
WARNING
- If the old cover is still displayed, you can try to clear the cache with the CTRL + SHIFT + R key
# Avoid browser caching
- When you upload a new cover, the browser will cache the image and probably won't show the new cover because it thinks it's already in his cache (the name of the file is not changed, only the content)
- To avoid this, you can add a random string to the end of the image url, eg: the current timestamp
- Update the src attribute of the
img
tag in thelivewire/admin/covers.blade.php
file- from:
$record->cover['url']
- to:
$record->cover['url'] . '?' . time()
- from:
<div x-data="" class="w-60 h-60 flex flex-col gap-4"> <img class="border border-gray-300 object-cover rounded shadow-lg" src="{{ $record->cover['url'] . '?v=' . time() }}" alt=""> <div class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" wire:click.prevent="openModal()" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> <a href="#" class="hover:text-white hover:bg-green-800 border-r border-gray-500">RESET</a> <a href="#" class="hover:text-white hover:bg-red-800">DELETE</a> </div> </div>
Copied!
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
# Reset the cover
- When the RESET button is clicked, the
resetCover()
method will be called
livewire/admin/covers.blade.php
Livewire/Admin/Covers.php
result
- Line 10: add the
getOriginalCover()
method to the RESET button
<div x-data="" class="w-60 h-60 flex flex-col gap-4"> <img class="border border-gray-300 object-cover rounded shadow-lg" src="{{ $record->cover['url'] . '?v=' . time() }}" alt=""> <div class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" wire:click.prevent="openModal()" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> <a href="#" wire:click.prevent="getOriginalCover()" class="hover:text-white hover:bg-green-800 border-r border-gray-500">RESET</a> <a href="#" class="hover:text-white hover:bg-red-800">DELETE</a> </div> </div>
Copied!
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
# Add a preloading text
- Fetching data from an external API can take a while, so it's a good idea to show a preloading text
livewire/admin/covers.blade.php
result
- Line 7 - 8: hide the buttons while the
getOriginalCover()
method is running - Line 21 - 27 add a preloading text
- Line 23 -24: text is only visible while the
getOriginalCover()
method is running
- Line 23 -24: text is only visible while the
<h1 class="text-3xl mb-4">Edit cover</h1> <div x-data="" class="w-60 h-60 flex flex-col gap-4"> <img class="border border-gray-300 object-cover rounded shadow-lg" src="{{ $record->cover['url'] . '?' . time() }}" alt=""> <div wire:loading.remove wire:target="getOriginalCover" class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" wire:click.prevent="openModal()" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> <a href="#" wire:click.prevent="getOriginalCover()" class="hover:text-white hover:bg-green-800 border-r border-gray-500">RESET</a> <a href="#" class="hover:text-white hover:bg-red-800">DELETE</a> </div> </div> {{-- Preloading text --}} <h3 wire:loading wire:target="getOriginalCover" class="text-teal-700"> Try to fetch original cover from the Cover Art Archive website </h3>
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
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
# Delete the cover
- When the DELETE button is clicked, the
deleteCover()
method will be called after a confirmation message
livewire/admin/covers.blade.php
Livewire/Admin/Covers.php
result
- Line 12 - 21: dispatch a
swal:confirm
dialog- Line 19: after the confirmation, the
deleteCover
event will be dispatched to the Livewire component
- Line 19: after the confirmation, the
<div wire:loading.remove wire:target="getOriginalCover" class="border border-gray-500 flex text-center [&>a]:flex-1 [&>a]:bg-gray-300 [&>a]:p-2 [&>a]:transition"> <a href="#" wire:click.prevent="openModal()" class="hover:text-white hover:bg-sky-800 border-r border-gray-500">EDIT</a> <a href="#" wire:click.prevent="getOriginalCover()" class="hover:text-white hover:bg-green-800 border-r border-gray-500">RESET</a> <a href="#" @click.prevent="$dispatch('swal:confirm', { title: 'Delete this cover?', cancelButtonText: 'NO', cancelButtonColor: '#3085d6', confirmButtonText: 'YES', confirmButtonColor: '#d33', next: { event: 'deleteCover' } })" class="hover:text-white hover:bg-red-800">DELETE</a> </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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23