# 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

# 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














 



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

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:
<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

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)
  • Open the file resources/views/livewire/admin/records.blade.php
  • Wrap the cover image in an anchor tag
    • add the attribute data-tippy-content with the value Edit cover
    • add the attribute href with the value route('admin.covers', $record->id)
      ($record->id is the parameter that will be passed to the Covers component)

 



 


<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

# Get the selected record

  • The mount() method of the Covers component has a parameter $id with a default value of null
    • If the parameter is not null, the mount() method will get the record with the given id from the database
    • If the parameter is null, the mount() method don't do anything for now (see part 2)
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

`find()` vs `findOrFail()`

  • To select one single record from the database, you can use the findOrFail() method or the find() method
  • If you try to select a record that doesn't exist:
    • the findOrFail() method will throw a 404 exception
    • the find() method will return null
  • Go to the URL http://vinyl_shop.test/admin/covers/999 (opens new window) and look at different results with find() and findOrFail()

# Upload a new cover

# Open the modal

  • Line 5: replace the dummy cover with the cover of the selected record
  • Line 9: add a wire:click.prevent attribute to the EDIT link
    • the wire:click.prevent attribute will call the openModal() method of the Covers component
    • the prevent modifier will prevent the default action of the link (e.g. following the href attribute)
  • 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 the saveCover() 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

# 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 the Covers component
  • Now you can use the wire:model attribute to bind the newCover property to the input element with the type="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()
  • 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

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 Show temp files
  • 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
  • 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 the newCover property, resize it to 250x250 pixels and encode it to a jpg 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
  • 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 and showModal 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

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 the livewire/admin/covers.blade.php file
    • from: $record->cover['url']
    • to: $record->cover['url'] . '?' . time()


 












<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

# Reset the cover

  • When the RESET button is clicked, the resetCover() method will be called
  • 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

# 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
  • 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






 
 












 
 
 
 
 
 
 

<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

# Delete the cover

  • When the DELETE button is clicked, the deleteCover() method will be called after a confirmation message
  • Line 12 - 21: dispatch a swal:confirm dialog
    • Line 19: after the confirmation, the deleteCover event will be dispatched to the Livewire component











 
 
 
 
 
 
 
 
 
 



<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
Last Updated: 3/6/2023, 8:01:02 PM