From 660c7da3944c305131f9c6316dc5b9332b7aeee7 Mon Sep 17 00:00:00 2001 From: Benjamin Takats Date: Tue, 6 Dec 2022 18:17:05 +0100 Subject: [PATCH] podcasts added --- .blueprint | 9 +- .../ReadAndSyncEinundzwanzigPodcastFeed.php | 55 ++++++ app/Http/Livewire/Frontend/Library.php | 22 ++- app/Http/Livewire/Tables/LibraryItemTable.php | 2 +- app/Models/Episode.php | 40 +++++ app/Models/LibraryItem.php | 5 + app/Models/Podcast.php | 32 ++++ app/Models/Tag.php | 5 + app/Nova/Episode.php | 156 ++++++++++++++++++ app/Nova/LibraryItem.php | 3 + app/Nova/Podcast.php | 118 +++++++++++++ app/Observers/EpisodeObserver.php | 68 ++++++++ app/Policies/EpisodePolicy.php | 101 ++++++++++++ app/Policies/PodcastPolicy.php | 94 +++++++++++ app/Providers/AppServiceProvider.php | 2 + app/Providers/EventServiceProvider.php | 8 +- app/Providers/NovaServiceProvider.php | 9 + composer.json | 1 + composer.lock | 33 +++- config/feeds/services.php | 8 + database/factories/EpisodeFactory.php | 31 ++++ database/factories/PodcastFactory.php | 31 ++++ ...022_12_04_154911_create_podcasts_table.php | 35 ++++ ...022_12_04_154912_create_episodes_table.php | 40 +++++ ...22_12_05_160932_create_libraries_table.php | 2 +- ...2_05_160933_create_library_items_table.php | 5 + database/seeders/DatabaseSeeder.php | 2 + .../columns/library_items/action.blade.php | 6 + .../columns/library_items/tags.blade.php | 4 +- .../views/livewire/frontend/library.blade.php | 7 +- 30 files changed, 910 insertions(+), 24 deletions(-) create mode 100644 app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php create mode 100644 app/Models/Episode.php create mode 100644 app/Models/Podcast.php create mode 100644 app/Nova/Episode.php create mode 100644 app/Nova/Podcast.php create mode 100644 app/Observers/EpisodeObserver.php create mode 100644 app/Policies/EpisodePolicy.php create mode 100644 app/Policies/PodcastPolicy.php create mode 100644 config/feeds/services.php create mode 100644 database/factories/EpisodeFactory.php create mode 100644 database/factories/PodcastFactory.php create mode 100644 database/migrations/2022_12_04_154911_create_podcasts_table.php create mode 100644 database/migrations/2022_12_04_154912_create_episodes_table.php diff --git a/.blueprint b/.blueprint index 891623c0..14741d32 100644 --- a/.blueprint +++ b/.blueprint @@ -1,16 +1,17 @@ -created: 'database/factories/LibraryFactory.php database/factories/LibraryItemsFactory.php database/migrations/2022_12_05_160932_create_libraries_table.php database/migrations/2022_12_05_160933_create_library_items_table.php app/Models/Library.php app/Models/LibraryItems.php app/Nova/Library.php app/Nova/LibraryItems.php' models: Category: { name: string, slug: string } City: { country_id: biginteger, name: string, slug: string, longitude: 'float:10', latitude: 'float:10' } - Country: { name: string, code: string } + Country: { name: string, code: string, language_codes: 'json default:[]' } Course: { lecturer_id: biginteger, name: string, description: 'text nullable' } + Episode: { guid: string, podcast_id: biginteger, data: json } Event: { course_id: biginteger, venue_id: biginteger, '"from"': datetime, '"to"': datetime, link: string } Lecturer: { team_id: biginteger, name: string, slug: string, active: 'boolean default:1', description: 'text nullable' } - Library: { name: string, language_code: string } - LibraryItem: { lecturer_id: biginteger, library_id: biginteger, order_column: integer, type: string, value: text } + Library: { name: string, is_public: 'boolean default:1', language_codes: 'json default:[]' } + LibraryItem: { lecturer_id: biginteger, episode_id: 'biginteger nullable', order_column: integer, name: string, type: string, language_code: string, value: 'text nullable' } LoginKey: { k1: string, user_id: biginteger } Membership: { team_id: biginteger, user_id: biginteger, role: 'string nullable' } Participant: { first_name: string, last_name: string } + Podcast: { guid: string, title: string, link: string, language_code: string, data: json } Registration: { event_id: biginteger, participant_id: biginteger, active: 'boolean default:1' } Tag: { name: json, slug: json, type: 'string nullable', order_column: 'integer nullable', icon: 'string default:tag' } Team: { user_id: biginteger, name: string, personal_team: boolean } diff --git a/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php b/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php new file mode 100644 index 00000000..7f71eaf4 --- /dev/null +++ b/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php @@ -0,0 +1,55 @@ + 'Einundzwanzig School', + 'key' => config('feeds.services.podcastindex-org.key'), + 'secret' => config('feeds.services.podcastindex-org.secret'), + ]); + $podcast = $client->podcasts->byFeedUrl('https://einundzwanzig.space/feed.xml') + ->json(); + $einundzwanzigPodcast = Podcast::query() + ->updateOrCreate(['guid' => $podcast->feed->podcastGuid], [ + 'title' => $podcast->feed->title, + 'link' => $podcast->feed->link, + 'language_code' => $podcast->feed->language, + 'data' => $podcast->feed, + ]); + $episodes = $client->episodes->byFeedUrl('https://einundzwanzig.space/feed.xml') + ->json(); + foreach ($episodes->items as $item) { + Episode::query() + ->updateOrCreate(['guid' => $item->guid], [ + 'podcast_id' => $einundzwanzigPodcast->id, + 'data' => $item, + ]); + } + + return Command::SUCCESS; + } +} diff --git a/app/Http/Livewire/Frontend/Library.php b/app/Http/Livewire/Frontend/Library.php index 796daee8..f0833e4a 100644 --- a/app/Http/Livewire/Frontend/Library.php +++ b/app/Http/Livewire/Frontend/Library.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Frontend; use App\Models\Country; +use App\Models\Podcast; use Livewire\Component; class Library extends Component @@ -24,13 +25,22 @@ class Library extends Component abort(403); } + $libraries = \App\Models\Library::query() + ->where('is_public', $shouldBePublic) + ->get(); + $tabs = collect([ + [ + 'name' => 'Alle', + ] + ]); + foreach ($libraries as $library) { + $tabs->push([ + 'name' => $library->name, + ]); + } + return view('livewire.frontend.library', [ - 'libraries' => \App\Models\Library::query() - ->where('is_public', $shouldBePublic) - ->get() - ->prepend(\App\Models\Library::make([ - 'name' => 'Alle', - ])), + 'libraries' => $tabs, ]); } } diff --git a/app/Http/Livewire/Tables/LibraryItemTable.php b/app/Http/Livewire/Tables/LibraryItemTable.php index 0a62b121..bd45a480 100644 --- a/app/Http/Livewire/Tables/LibraryItemTable.php +++ b/app/Http/Livewire/Tables/LibraryItemTable.php @@ -93,7 +93,7 @@ class LibraryItemTable extends DataTableComponent 'alt' => $row->name.' Avatar', ]) ->collapseOnMobile(), - Column::make('Dozent', "lecturer.name") + Column::make('Ersteller', "lecturer.name") ->label( fn($row, Column $column) => view('columns.courses.lecturer')->withRow($row) ) diff --git a/app/Models/Episode.php b/app/Models/Episode.php new file mode 100644 index 00000000..4b62d5e7 --- /dev/null +++ b/app/Models/Episode.php @@ -0,0 +1,40 @@ + 'integer', + 'podcast_id' => 'integer', + 'data' => 'array', + ]; + + public function podcast(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Podcast::class); + } + + public function libraryItem(): HasOne + { + return $this->hasOne(LibraryItem::class); + } +} diff --git a/app/Models/LibraryItem.php b/app/Models/LibraryItem.php index bf513037..7a813afa 100644 --- a/app/Models/LibraryItem.php +++ b/app/Models/LibraryItem.php @@ -64,6 +64,11 @@ class LibraryItem extends Model implements HasMedia, Sortable return $this->belongsTo(Lecturer::class); } + public function episode(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Episode::class); + } + public function libraries(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(Library::class); diff --git a/app/Models/Podcast.php b/app/Models/Podcast.php new file mode 100644 index 00000000..c6e8c506 --- /dev/null +++ b/app/Models/Podcast.php @@ -0,0 +1,32 @@ + 'integer', + 'data' => 'array', + ]; + + public function episodes(): HasMany + { + return $this->hasMany(Episode::class); + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 42bed46f..bbbc63cb 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -13,4 +13,9 @@ class Tag extends \Spatie\Tags\Tag { return $this->morphedByMany(LibraryItem::class, 'taggable'); } + + public function episodes() + { + return $this->morphedByMany(Episode::class, 'taggable'); + } } diff --git a/app/Nova/Episode.php b/app/Nova/Episode.php new file mode 100644 index 00000000..dada5eb0 --- /dev/null +++ b/app/Nova/Episode.php @@ -0,0 +1,156 @@ +tags) { + $lecturer = \App\Models\Lecturer::updateOrCreate(['name' => $model->podcast->title], [ + 'team_id' => 1, + 'active' => true, + ]); + $lecturer->addMediaFromUrl($model->podcast->data['image']) + ->toMediaCollection('avatar'); + $library = \App\Models\Library::updateOrCreate( + [ + 'name' => $model->podcast->title + ], + [ + 'language_codes' => [$model->podcast->language_code], + ]); + $libraryItem = $model->libraryItem() + ->firstOrCreate([ + 'lecturer_id' => $lecturer->id, + 'episode_id' => $model->id, + 'name' => $model->data['title'], + 'type' => 'podcast_episode', + 'language_code' => $model->podcast->language_code, + 'value' => null, + ]); + ray($request->tags); + $libraryItem->syncTagsWithType(is_array($request->tags) ? $request->tags : str($request->tags)->explode('-----'), + 'library_item'); + $libraryItem->addMediaFromUrl($model->data['image']) + ->toMediaCollection('main'); + $library->libraryItems() + ->attach($libraryItem); + } + } + + public function title() + { + return $this->data['title']; + } + + /** + * Get the fields displayed by the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function fields(Request $request) + { + return [ + ID::make() + ->sortable(), + + Avatar::make('Image') + ->squared() + ->thumbnail(function () { + return $this->data['image']; + }) + ->exceptOnForms(), + + Tags::make('Tags') + ->type('library_item') + ->withLinkToTagResource(Tag::class), + + Text::make('Title', 'data->title') + ->readonly() + ->rules('required', 'string'), + + Code::make('Data') + ->readonly() + ->rules('required', 'json') + ->json(), + + BelongsTo::make('Podcast') + ->readonly(), + + ]; + } + + /** + * Get the cards available for the request. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function cards(Request $request) + { + return []; + } + + /** + * Get the filters available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function filters(Request $request) + { + return []; + } + + /** + * Get the lenses available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function lenses(Request $request) + { + return []; + } + + /** + * Get the actions available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function actions(Request $request) + { + return []; + } +} diff --git a/app/Nova/LibraryItem.php b/app/Nova/LibraryItem.php index 692d06dc..27bfda1b 100644 --- a/app/Nova/LibraryItem.php +++ b/app/Nova/LibraryItem.php @@ -80,6 +80,7 @@ class LibraryItem extends Resource 'markdown_article' => 'markdown_article', 'youtube_video' => 'youtube_video', 'vimeo_video' => 'vimeo_video', + 'podcast_episode' => 'podcast_episode', 'downloadable_file' => 'downloadable_file', ] ) @@ -91,6 +92,8 @@ class LibraryItem extends Resource BelongsTo::make('Lecturer'), + BelongsTo::make('Episode'), + BelongsToMany::make('Library', 'libraries', Library::class), ]; diff --git a/app/Nova/Podcast.php b/app/Nova/Podcast.php new file mode 100644 index 00000000..8ae8875b --- /dev/null +++ b/app/Nova/Podcast.php @@ -0,0 +1,118 @@ +sortable(), + + Avatar::make('Image') + ->squared() + ->thumbnail(function () { + return $this->data['image']; + }), + + Text::make('Title') + ->rules('required', 'string'), + + Text::make('Language Code') + ->rules('required', 'string'), + + Text::make('Link') + ->rules('required', 'string'), + + Code::make('Data') + ->rules('required', 'json') + ->json(), + + HasMany::make('Episodes'), + ]; + } + + /** + * Get the cards available for the request. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function cards(Request $request) + { + return []; + } + + /** + * Get the filters available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function filters(Request $request) + { + return []; + } + + /** + * Get the lenses available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function lenses(Request $request) + { + return []; + } + + /** + * Get the actions available for the resource. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function actions(Request $request) + { + return []; + } +} diff --git a/app/Observers/EpisodeObserver.php b/app/Observers/EpisodeObserver.php new file mode 100644 index 00000000..0b5f8e72 --- /dev/null +++ b/app/Observers/EpisodeObserver.php @@ -0,0 +1,68 @@ +hasRole('super-admin'); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $user + * @param \App\Models\Episode $episode + * + * @return \Illuminate\Auth\Access\Response|bool + */ + public function delete(User $user, Episode $episode) + { + return false; + } + + /** + * Determine whether the user can restore the model. + * + * @param \App\Models\User $user + * @param \App\Models\Episode $episode + * + * @return \Illuminate\Auth\Access\Response|bool + */ + public function restore(User $user, Episode $episode) + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param \App\Models\User $user + * @param \App\Models\Episode $episode + * + * @return \Illuminate\Auth\Access\Response|bool + */ + public function forceDelete(User $user, Episode $episode) + { + return false; + } +} diff --git a/app/Policies/PodcastPolicy.php b/app/Policies/PodcastPolicy.php new file mode 100644 index 00000000..cbe691b7 --- /dev/null +++ b/app/Policies/PodcastPolicy.php @@ -0,0 +1,94 @@ +> */ protected $listen = [ @@ -23,17 +21,15 @@ class EventServiceProvider extends ServiceProvider /** * Register any events for your application. - * * @return void */ public function boot() { - \App\Models\Event::observe(EventObserver::class); + // } /** * Determine if events and listeners should be automatically discovered. - * * @return bool */ public function shouldDiscoverEvents() diff --git a/app/Providers/NovaServiceProvider.php b/app/Providers/NovaServiceProvider.php index d1eb5ef8..b7ecbeb4 100644 --- a/app/Providers/NovaServiceProvider.php +++ b/app/Providers/NovaServiceProvider.php @@ -7,11 +7,13 @@ use App\Nova\City; use App\Nova\Country; use App\Nova\Course; use App\Nova\Dashboards\Main; +use App\Nova\Episode; use App\Nova\Event; use App\Nova\Lecturer; use App\Nova\Library; use App\Nova\LibraryItem; use App\Nova\Participant; +use App\Nova\Podcast; use App\Nova\Registration; use App\Nova\Tag; use App\Nova\Team; @@ -59,6 +61,13 @@ class NovaServiceProvider extends NovaApplicationServiceProvider ->icon('library') ->collapsable(), + MenuSection::make('Podcasts', [ + MenuItem::resource(Podcast::class), + MenuItem::resource(Episode::class), + ]) + ->icon('microphone') + ->collapsable(), + MenuSection::make('Admin', [ MenuItem::resource(Category::class), MenuItem::resource(Country::class), diff --git a/composer.json b/composer.json index b23136a2..83212a3b 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "laravel/tinker": "^2.7", "livewire/livewire": "^2.5", "nova/start": "*", + "podcastindex/podcastindex-php": "^1.0", "rappasoft/laravel-livewire-tables": "^2.8", "sentry/sentry-laravel": "^3.1", "simplesoftwareio/simple-qrcode": "^4.2", diff --git a/composer.lock b/composer.lock index 88d4fb99..f33b9562 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fe13b4c19e33aebc3926e4dcd5ddf8e8", + "content-hash": "9c4898684e578e46bb709340edf618e8", "packages": [ { "name": "akuechler/laravel-geoly", @@ -4941,6 +4941,37 @@ }, "time": "2021-10-28T11:13:42+00:00" }, + { + "name": "podcastindex/podcastindex-php", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/LowSociety/podcastindex-php.git", + "reference": "8aa323cf67e1892c8ebec4500803fb8bf14ba84b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LowSociety/podcastindex-php/zipball/8aa323cf67e1892c8ebec4500803fb8bf14ba84b", + "reference": "8aa323cf67e1892c8ebec4500803fb8bf14ba84b", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PodcastIndex\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "description": "A PHP wrapper for the PodcastIndex API.", + "support": { + "issues": "https://github.com/LowSociety/podcastindex-php/issues", + "source": "https://github.com/LowSociety/podcastindex-php/tree/1.0.0" + }, + "time": "2020-09-10T10:09:37+00:00" + }, { "name": "pragmarx/google2fa", "version": "v8.0.1", diff --git a/config/feeds/services.php b/config/feeds/services.php new file mode 100644 index 00000000..287878bd --- /dev/null +++ b/config/feeds/services.php @@ -0,0 +1,8 @@ + [ + 'key' => env('PODCASTINDEX_ORG_KEY'), + 'secret' => env('PODCASTINDEX_ORG_SECRET'), + ] +]; diff --git a/database/factories/EpisodeFactory.php b/database/factories/EpisodeFactory.php new file mode 100644 index 00000000..3076fd50 --- /dev/null +++ b/database/factories/EpisodeFactory.php @@ -0,0 +1,31 @@ + Podcast::factory(), + 'data' => '{}', + ]; + } +} diff --git a/database/factories/PodcastFactory.php b/database/factories/PodcastFactory.php new file mode 100644 index 00000000..90ace2ee --- /dev/null +++ b/database/factories/PodcastFactory.php @@ -0,0 +1,31 @@ + $this->faker->sentence(4), + 'link' => $this->faker->word, + 'data' => '{}', + ]; + } +} diff --git a/database/migrations/2022_12_04_154911_create_podcasts_table.php b/database/migrations/2022_12_04_154911_create_podcasts_table.php new file mode 100644 index 00000000..759b87a1 --- /dev/null +++ b/database/migrations/2022_12_04_154911_create_podcasts_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('guid') + ->unique(); + $table->string('title'); + $table->string('link'); + $table->string('language_code'); + $table->json('data'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down(): void + { + Schema::dropIfExists('podcasts'); + } +} diff --git a/database/migrations/2022_12_04_154912_create_episodes_table.php b/database/migrations/2022_12_04_154912_create_episodes_table.php new file mode 100644 index 00000000..981a1b85 --- /dev/null +++ b/database/migrations/2022_12_04_154912_create_episodes_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('guid') + ->unique(); + $table->foreignId('podcast_id') + ->constrained() + ->cascadeOnDelete() + ->cascadeOnUpdate(); + $table->json('data'); + $table->timestamps(); + }); + + Schema::enableForeignKeyConstraints(); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down(): void + { + Schema::dropIfExists('episodes'); + } +} diff --git a/database/migrations/2022_12_05_160932_create_libraries_table.php b/database/migrations/2022_12_05_160932_create_libraries_table.php index e79296ff..ea72aa87 100644 --- a/database/migrations/2022_12_05_160932_create_libraries_table.php +++ b/database/migrations/2022_12_05_160932_create_libraries_table.php @@ -14,7 +14,7 @@ class CreateLibrariesTable extends Migration { Schema::create('libraries', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->boolean('is_public') ->default(true); $table->json('language_codes') diff --git a/database/migrations/2022_12_05_160933_create_library_items_table.php b/database/migrations/2022_12_05_160933_create_library_items_table.php index d2cd8106..e0c21c72 100644 --- a/database/migrations/2022_12_05_160933_create_library_items_table.php +++ b/database/migrations/2022_12_05_160933_create_library_items_table.php @@ -20,6 +20,11 @@ class CreateLibraryItemsTable extends Migration ->constrained() ->cascadeOnDelete() ->cascadeOnUpdate(); + $table->foreignId('episode_id') + ->nullable() + ->constrained() + ->cascadeOnDelete() + ->cascadeOnUpdate(); $table->unsignedInteger('order_column'); $table->string('name'); $table->string('type'); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e9e9adbe..1788fcda 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use App\Console\Commands\Database\CreateTags; +use App\Console\Commands\Feed\ReadAndSyncEinundzwanzigPodcastFeed; use App\Models\Category; use App\Models\City; use App\Models\Country; @@ -277,5 +278,6 @@ class DatabaseSeeder extends Seeder $libraryItem->syncTagsWithType(['Präsentationen'], 'library_item'); $nonPublicLibrary->libraryItems() ->attach($libraryItem); + Artisan::call(ReadAndSyncEinundzwanzigPodcastFeed::class); } } diff --git a/resources/views/columns/library_items/action.blade.php b/resources/views/columns/library_items/action.blade.php index 3522dd23..430d54d2 100644 --- a/resources/views/columns/library_items/action.blade.php +++ b/resources/views/columns/library_items/action.blade.php @@ -11,4 +11,10 @@ Download @endif + @if($row->type === 'podcast_episode') + + + Anhören + + @endif diff --git a/resources/views/columns/library_items/tags.blade.php b/resources/views/columns/library_items/tags.blade.php index ade0e598..9aa5cfaa 100644 --- a/resources/views/columns/library_items/tags.blade.php +++ b/resources/views/columns/library_items/tags.blade.php @@ -1,5 +1,5 @@ -
+
@foreach($row->tags as $tag) - {{ $tag->name }} + {{ $tag->name }} @endforeach
diff --git a/resources/views/livewire/frontend/library.blade.php b/resources/views/livewire/frontend/library.blade.php index 69301ad4..84442bf7 100644 --- a/resources/views/livewire/frontend/library.blade.php +++ b/resources/views/livewire/frontend/library.blade.php @@ -30,10 +30,10 @@
@@ -42,6 +42,7 @@
+