Media Library - v1.1.0
Integration Examples
Complete examples for integrating the Media Library with visual editors and CMS platforms.
Blog Post Editor
A full-featured blog post editor with featured image and gallery support.
Livewire Component
<?php
namespace App\Livewire;
use ArtisanPackUI\MediaLibrary\Models\Media;
use Livewire\Attributes\On;
use Livewire\Component;
class BlogPostEditor extends Component
{
public $post;
public $title = '';
public $content = '';
public $featuredImageId = null;
public $galleryImageIds = [];
public function mount($post = null)
{
if ($post) {
$this->post = $post;
$this->title = $post->title;
$this->content = $post->content;
$this->featuredImageId = $post->featured_image_id;
$this->galleryImageIds = $post->gallery_images ?? [];
}
}
#[On('media-picked')]
public function handleMediaPicked(array $media, string $context): void
{
match ($context) {
'featured-image' => $this->setFeaturedImage($media[0]['id']),
'gallery' => $this->addGalleryImages(collect($media)->pluck('id')->toArray()),
default => null,
};
}
public function setFeaturedImage(int $mediaId): void
{
$this->featuredImageId = $mediaId;
}
public function removeFeaturedImage(): void
{
$this->featuredImageId = null;
}
public function addGalleryImages(array $mediaIds): void
{
$this->galleryImageIds = array_unique(
array_merge($this->galleryImageIds, $mediaIds)
);
}
public function removeGalleryImage(int $mediaId): void
{
$this->galleryImageIds = array_values(
array_diff($this->galleryImageIds, [$mediaId])
);
}
public function reorderGallery(array $order): void
{
$this->galleryImageIds = $order;
}
public function save(): void
{
$validated = $this->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$data = [
...$validated,
'featured_image_id' => $this->featuredImageId,
'gallery_images' => $this->galleryImageIds,
];
if ($this->post) {
$this->post->update($data);
} else {
$this->post = auth()->user()->posts()->create($data);
}
$this->dispatch('post-saved');
}
public function render()
{
return view('livewire.blog-post-editor', [
'featuredImage' => $this->featuredImageId
? Media::find($this->featuredImageId)
: null,
'galleryImages' => Media::whereIn('id', $this->galleryImageIds)
->orderByRaw('FIELD(id, ' . implode(',', $this->galleryImageIds ?: [0]) . ')')
->get(),
]);
}
}
Blade Template
{{-- resources/views/livewire/blog-post-editor.blade.php --}}
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">
{{ $post ? 'Edit Post' : 'Create Post' }}
</h1>
<form wire:submit="save" class="space-y-6">
<!-- Title -->
<x-artisanpack-input
label="Title"
wire:model="title"
placeholder="Enter post title..."
:error="$errors->first('title')"
/>
<!-- Featured Image -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Featured Image</span>
</label>
@if($featuredImage)
<div class="relative inline-block">
<img
src="{{ $featuredImage->imageUrl('medium') }}"
alt="{{ $featuredImage->alt_text }}"
class="rounded-lg max-w-md"
/>
<button
type="button"
wire:click="removeFeaturedImage"
class="btn btn-circle btn-sm btn-error absolute top-2 right-2"
>
<x-artisanpack-icon name="o-x-mark" class="w-4 h-4" />
</button>
</div>
@else
<button
type="button"
@click="$dispatch('open-media-picker', { context: 'featured-image' })"
class="btn btn-outline"
>
<x-artisanpack-icon name="o-photo" class="w-5 h-5 mr-2" />
Select Featured Image
</button>
@endif
</div>
<!-- Content -->
<x-artisanpack-rich-text-editor
label="Content"
wire:model="content"
:error="$errors->first('content')"
/>
<!-- Gallery -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Gallery Images</span>
<span class="label-text-alt">{{ count($galleryImageIds) }}/20 images</span>
</label>
<div
class="grid grid-cols-4 gap-4"
x-data="{ dragging: null }"
x-on:dragover.prevent
>
@foreach($galleryImages as $index => $image)
<div
class="relative group cursor-move"
draggable="true"
x-on:dragstart="dragging = {{ $image->id }}"
x-on:dragend="dragging = null"
x-on:drop="$wire.reorderGallery([...document.querySelectorAll('[data-gallery-id]')].map(el => parseInt(el.dataset.galleryId)))"
data-gallery-id="{{ $image->id }}"
>
<img
src="{{ $image->imageUrl('thumbnail') }}"
alt="{{ $image->alt_text }}"
class="rounded aspect-square object-cover w-full"
/>
<button
type="button"
wire:click="removeGalleryImage({{ $image->id }})"
class="btn btn-circle btn-xs btn-error absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<x-artisanpack-icon name="o-x-mark" class="w-3 h-3" />
</button>
<div class="absolute bottom-1 left-1 badge badge-sm badge-neutral">
{{ $index + 1 }}
</div>
</div>
@endforeach
@if(count($galleryImageIds) < 20)
<button
type="button"
@click="$dispatch('open-media-picker', { context: 'gallery' })"
class="aspect-square border-2 border-dashed border-base-300 rounded flex flex-col items-center justify-center hover:border-primary transition-colors"
>
<x-artisanpack-icon name="o-plus" class="w-8 h-8 text-base-content/50" />
<span class="text-sm text-base-content/50 mt-1">Add Images</span>
</button>
@endif
</div>
</div>
<!-- Actions -->
<div class="flex justify-end gap-2">
<x-artisanpack-button type="button" color="ghost">
Cancel
</x-artisanpack-button>
<x-artisanpack-button type="submit" color="primary">
{{ $post ? 'Update Post' : 'Create Post' }}
</x-artisanpack-button>
</div>
</form>
<!-- Media Pickers -->
<livewire:media-picker
context="featured-image"
:allowed-types="['image']"
/>
<livewire:media-picker
context="gallery"
:allowed-types="['image']"
:multi-select="true"
:max-selections="20 - count($galleryImageIds)"
/>
</div>
Page Builder with Blocks
A visual page builder with multiple block types.
Block Configuration
// config/artisanpack.php
'media' => [
'block_requirements' => [
'hero' => [
'allowed_types' => ['image'],
'max_file_size' => 10240,
'max_selections' => 1,
'min_width' => 1920,
'min_height' => 600,
],
'text-with-image' => [
'allowed_types' => ['image'],
'max_file_size' => 5120,
'max_selections' => 1,
],
'gallery' => [
'allowed_types' => ['image'],
'max_file_size' => 5120,
'max_selections' => 12,
],
'video' => [
'allowed_types' => ['video'],
'max_file_size' => 102400,
'max_selections' => 1,
],
'testimonial' => [
'allowed_types' => ['image'],
'max_file_size' => 1024,
'max_selections' => 1,
],
],
],
Page Builder Component
<?php
namespace App\Livewire;
use ArtisanPackUI\MediaLibrary\Models\Media;
use Livewire\Attributes\On;
use Livewire\Component;
class PageBuilder extends Component
{
public $page;
public $blocks = [];
public $activeBlockId = null;
public $activeFieldName = null;
public function mount($page)
{
$this->page = $page;
$this->blocks = $page->blocks ?? [];
}
public function openMediaPicker(string $blockId, string $fieldName): void
{
$this->activeBlockId = $blockId;
$this->activeFieldName = $fieldName;
$block = collect($this->blocks)->firstWhere('id', $blockId);
$requirements = apGetBlockRequirements($block['type']);
$this->dispatch('open-media-picker', [
'context' => "block-{$blockId}-{$fieldName}",
'allowedTypes' => $requirements['allowed_types'] ?? ['image'],
'maxSelections' => $requirements['max_selections'] ?? 1,
]);
}
#[On('media-picked')]
public function handleMediaPicked(array $media, string $context): void
{
if (!str_starts_with($context, 'block-')) {
return;
}
$parts = explode('-', $context);
$blockId = $parts[1];
$fieldName = $parts[2];
$this->blocks = collect($this->blocks)->map(function ($block) use ($blockId, $fieldName, $media) {
if ($block['id'] === $blockId) {
$block['content'][$fieldName] = count($media) === 1
? $media[0]['id']
: collect($media)->pluck('id')->toArray();
}
return $block;
})->toArray();
}
public function addBlock(string $type): void
{
$this->blocks[] = [
'id' => uniqid('block_'),
'type' => $type,
'content' => [],
];
}
public function removeBlock(string $blockId): void
{
$this->blocks = collect($this->blocks)
->reject(fn ($block) => $block['id'] === $blockId)
->values()
->toArray();
}
public function save(): void
{
$this->page->update(['blocks' => $this->blocks]);
$this->dispatch('page-saved');
}
public function render()
{
return view('livewire.page-builder');
}
}
Page Builder Template
{{-- resources/views/livewire/page-builder.blade.php --}}
<div class="flex h-screen">
<!-- Block Library Sidebar -->
<aside class="w-64 bg-base-200 p-4 overflow-y-auto">
<h2 class="font-bold mb-4">Add Block</h2>
<div class="space-y-2">
@foreach(['hero', 'text-with-image', 'gallery', 'video', 'testimonial'] as $type)
<button
wire:click="addBlock('{{ $type }}')"
class="btn btn-sm btn-block justify-start"
>
<x-artisanpack-icon name="{{ $blockIcons[$type] ?? 'o-square-3-stack-3d' }}" class="w-4 h-4" />
{{ Str::title(str_replace('-', ' ', $type)) }}
</button>
@endforeach
</div>
</aside>
<!-- Canvas -->
<main class="flex-1 overflow-y-auto p-8 bg-base-100">
<div class="max-w-4xl mx-auto space-y-6">
@forelse($blocks as $index => $block)
<div
class="border rounded-lg p-4 relative group"
wire:key="block-{{ $block['id'] }}"
>
<!-- Block Header -->
<div class="flex items-center justify-between mb-4">
<span class="badge">{{ Str::title(str_replace('-', ' ', $block['type'])) }}</span>
<button
wire:click="removeBlock('{{ $block['id'] }}')"
class="btn btn-ghost btn-xs text-error opacity-0 group-hover:opacity-100"
>
Remove
</button>
</div>
<!-- Block Content Editor -->
@include("blocks.editors.{$block['type']}", ['block' => $block])
</div>
@empty
<div class="text-center py-12 text-base-content/50">
<p>No blocks yet. Add a block from the sidebar.</p>
</div>
@endforelse
</div>
</main>
<!-- Dynamic Media Picker -->
<livewire:media-picker context="block-media" />
</div>
Block Editor Partials
{{-- resources/views/blocks/editors/hero.blade.php --}}
<div class="space-y-4">
<!-- Background Image -->
<div>
<label class="label">Background Image</label>
@php $media = isset($block['content']['image']) ? \ArtisanPackUI\MediaLibrary\Models\Media::find($block['content']['image']) : null; @endphp
@if($media)
<div class="relative">
<img src="{{ $media->imageUrl('medium') }}" class="rounded w-full h-48 object-cover" />
<button
type="button"
wire:click="$set('blocks.{{ $loop->index }}.content.image', null)"
class="btn btn-circle btn-sm btn-error absolute top-2 right-2"
>
<x-artisanpack-icon name="o-x-mark" class="w-4 h-4" />
</button>
</div>
@else
<button
type="button"
wire:click="openMediaPicker('{{ $block['id'] }}', 'image')"
class="btn btn-outline w-full h-32"
>
Select Hero Image (1920x600 min)
</button>
@endif
</div>
<!-- Title -->
<x-artisanpack-input
label="Title"
wire:model.live="blocks.{{ $loop->index }}.content.title"
/>
<!-- Subtitle -->
<x-artisanpack-textarea
label="Subtitle"
wire:model.live="blocks.{{ $loop->index }}.content.subtitle"
rows="2"
/>
</div>
{{-- resources/views/blocks/editors/gallery.blade.php --}}
<div>
<label class="label">Gallery Images (up to 12)</label>
@php
$imageIds = $block['content']['images'] ?? [];
$images = \ArtisanPackUI\MediaLibrary\Models\Media::whereIn('id', $imageIds)->get();
@endphp
<div class="grid grid-cols-4 gap-2">
@foreach($images as $image)
<div class="relative aspect-square">
<img src="{{ $image->imageUrl('thumbnail') }}" class="rounded object-cover w-full h-full" />
<button
type="button"
wire:click="$set('blocks.{{ $loop->parent->index }}.content.images', {{ json_encode(array_diff($imageIds, [$image->id])) }})"
class="btn btn-circle btn-xs btn-error absolute top-1 right-1"
>
<x-artisanpack-icon name="o-x-mark" class="w-3 h-3" />
</button>
</div>
@endforeach
@if(count($images) < 12)
<button
type="button"
wire:click="openMediaPicker('{{ $block['id'] }}', 'images')"
class="aspect-square border-2 border-dashed rounded flex items-center justify-center"
>
<x-artisanpack-icon name="o-plus" class="w-6 h-6" />
</button>
@endif
</div>
</div>
Product Editor
E-commerce product editor with image management.
<?php
namespace App\Livewire;
use App\Models\Product;
use ArtisanPackUI\MediaLibrary\Models\Media;
use Livewire\Attributes\On;
use Livewire\Component;
class ProductEditor extends Component
{
public Product $product;
public array $imageIds = [];
protected $rules = [
'product.name' => 'required|string|max:255',
'product.description' => 'required|string',
'product.price' => 'required|numeric|min:0',
'imageIds' => 'array|min:1',
'imageIds.*' => 'exists:media,id',
];
public function mount(Product $product)
{
$this->product = $product;
$this->imageIds = $product->images()->pluck('media.id')->toArray();
}
#[On('media-picked')]
public function handleMediaPicked(array $media, string $context): void
{
if ($context !== 'product-images') {
return;
}
$this->imageIds = array_unique(array_merge(
$this->imageIds,
collect($media)->pluck('id')->toArray()
));
}
public function removeImage(int $mediaId): void
{
$this->imageIds = array_values(array_diff($this->imageIds, [$mediaId]));
}
public function setPrimaryImage(int $mediaId): void
{
// Move to first position
$this->imageIds = array_values(array_diff($this->imageIds, [$mediaId]));
array_unshift($this->imageIds, $mediaId);
}
public function save(): void
{
$this->validate();
$this->product->save();
$this->product->syncImages($this->imageIds);
$this->dispatch('product-saved');
}
public function render()
{
return view('livewire.product-editor', [
'images' => Media::whereIn('id', $this->imageIds)
->orderByRaw('FIELD(id, ' . implode(',', $this->imageIds ?: [0]) . ')')
->get(),
]);
}
}
{{-- resources/views/livewire/product-editor.blade.php --}}
<div class="grid grid-cols-2 gap-8">
<!-- Images -->
<div>
<h2 class="text-lg font-bold mb-4">Product Images</h2>
@if($images->isNotEmpty())
<!-- Primary Image -->
<div class="mb-4">
<img
src="{{ $images->first()->imageUrl('large') }}"
alt="{{ $images->first()->alt_text }}"
class="rounded-lg w-full aspect-square object-cover"
/>
</div>
<!-- Thumbnail Grid -->
<div class="grid grid-cols-4 gap-2">
@foreach($images as $index => $image)
<div
class="relative aspect-square cursor-pointer {{ $index === 0 ? 'ring-2 ring-primary ring-offset-2' : '' }}"
wire:click="setPrimaryImage({{ $image->id }})"
>
<img
src="{{ $image->imageUrl('thumbnail') }}"
class="rounded object-cover w-full h-full"
/>
<button
wire:click.stop="removeImage({{ $image->id }})"
class="btn btn-circle btn-xs btn-error absolute -top-1 -right-1"
>
<x-artisanpack-icon name="o-x-mark" class="w-3 h-3" />
</button>
@if($index === 0)
<span class="absolute bottom-1 left-1 badge badge-primary badge-xs">Primary</span>
@endif
</div>
@endforeach
<button
type="button"
@click="$dispatch('open-media-picker', { context: 'product-images' })"
class="aspect-square border-2 border-dashed rounded flex items-center justify-center hover:border-primary"
>
<x-artisanpack-icon name="o-plus" class="w-6 h-6" />
</button>
</div>
@else
<button
type="button"
@click="$dispatch('open-media-picker', { context: 'product-images' })"
class="w-full aspect-square border-2 border-dashed rounded-lg flex flex-col items-center justify-center hover:border-primary"
>
<x-artisanpack-icon name="o-photo" class="w-12 h-12 mb-2" />
<span>Add Product Images</span>
</button>
@endif
@error('imageIds')
<p class="text-error text-sm mt-2">{{ $message }}</p>
@enderror
</div>
<!-- Details -->
<div class="space-y-4">
<h2 class="text-lg font-bold">Product Details</h2>
<x-artisanpack-input
label="Product Name"
wire:model="product.name"
:error="$errors->first('product.name')"
/>
<x-artisanpack-textarea
label="Description"
wire:model="product.description"
:error="$errors->first('product.description')"
rows="4"
/>
<x-artisanpack-input
label="Price"
wire:model="product.price"
type="number"
step="0.01"
prefix="$"
:error="$errors->first('product.price')"
/>
<x-artisanpack-button wire:click="save" color="primary" class="w-full">
Save Product
</x-artisanpack-button>
</div>
<!-- Media Picker -->
<livewire:media-picker
context="product-images"
:allowed-types="['image']"
:multi-select="true"
:max-selections="10"
/>
</div>
Next Steps
- MediaPicker Component - Detailed component documentation
- Block Content Helpers - Working with block content
- Configuration - All configuration options