Skip to content

Commit d65436a

Browse files
committed
Initial commit
0 parents  commit d65436a

19 files changed

+1986
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
vendor/
2+
.idea/
3+
composer.lock
4+
.php_cs.cache
5+
build/

.phpunit.cache/test-results

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"pest_2.36.0","defects":[],"times":{"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_renders_the_component":0.029,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_uploads_a_single_image_and_lists_it":0.037,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_renames_on_name_conflict_by_default":0.028,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_replaces_on_name_conflict_when_configured":0.03,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_skips_exact_duplicates_when_enabled":0.027,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_updates_metadata_via_inline_edit":0.029,"P\\Tests\\Feature\\MediaUploaderTest::__pest_evaluable_it_deletes_media_via_confirmation_flow":0.024}}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
All notable changes to `livewire-media-uploader` will be documented here.
4+
5+
## 1.0 - Not released yet
6+
7+
### Initial Release

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 RJ
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
# Livewire Media Uploader
2+
3+
Reusable **Livewire v3** media uploader with **TailwindCSS** UI, Alpine-powered overlays, and first-class integration with **Spatie Laravel Media Library**. Ships with a **publishable Blade view** so each app can theme it as needed.
4+
5+
---
6+
7+
## Table of Contents
8+
9+
- [Features](#features)
10+
- [Requirements](#requirements)
11+
- [Installation](#installation)
12+
- [Publishing Assets](#publishing-assets)
13+
- [Quick Start](#quick-start)
14+
- [Usage Examples](#usage-examples)
15+
- [Configuration](#configuration)
16+
- [Props](#props)
17+
- [Events](#events)
18+
- [Model Setup (Spatie Media Library)](#model-setup-spatie-media-library)
19+
- [Overlays & UX Notes](#overlays--ux-notes)
20+
- [Troubleshooting](#troubleshooting)
21+
- [Roadmap](#roadmap)
22+
- [License](#license)
23+
24+
---
25+
26+
## Features
27+
28+
- ✅ Livewire v3 component with Tailwind-only Blade (no UI dependency)
29+
- ✅ Spatie Media Library integration (attach, list, edit meta, delete)
30+
-**Publishable view** for per-project customization
31+
- ✅ Drag & drop uploads + progress bar
32+
- ✅ Inline edit of **caption / description / order**
33+
- ✅ Name-conflict strategies: **rename | replace | skip | allow**
34+
- ✅ Optional **exact duplicate** detection via SHA-256
35+
- ✅ Collection → preset mapping (auto `accept` attribute)
36+
- ✅ Image preview **overlay** + delete confirmation **modal**
37+
- ✅ Works with:
38+
- Saved model instance (`:for="$model"`)
39+
- String model + id (`model="user" :id="1"`)
40+
- FQCN, morph map alias, or dotted paths with custom namespaces
41+
- Local alias map
42+
43+
---
44+
45+
## Requirements
46+
47+
- PHP **8.1+**
48+
- Laravel **10.x | 11.x | 12.x**
49+
- Livewire **^3.0**
50+
- spatie/laravel-medialibrary **^10.12**
51+
- TailwindCSS (optional but recommended for the default view)
52+
- Alpine.js (used by overlays/progress; see [Overlays & UX Notes](#overlays--ux-notes))
53+
54+
---
55+
56+
## Installation
57+
58+
```bash
59+
composer require codebyray/livewire-media-uploader
60+
```
61+
62+
Auto-discovery will register the service provider. If you disable discovery, add:
63+
64+
```php
65+
// config/app.php
66+
'providers' => [
67+
// ...
68+
Codebyray\LivewireMediaUploader\MediaUploaderServiceProvider::class,
69+
],
70+
```
71+
72+
The component is registered under **both** aliases:
73+
74+
- `<livewire:media-uploader ... />`
75+
- `<livewire:media.media-uploader ... />`
76+
77+
---
78+
79+
## Publishing Assets
80+
81+
**Config:**
82+
```bash
83+
php artisan vendor:publish --tag=media-uploader-config
84+
```
85+
86+
**Views:**
87+
```bash
88+
php artisan vendor:publish --tag=media-uploader-views
89+
```
90+
91+
After publishing, customize the Blade at:
92+
```
93+
resources/views/vendor/media-uploader/livewire/media-uploader.blade.php
94+
```
95+
96+
---
97+
98+
## Quick Start
99+
100+
1) Ensure your target Eloquent model implements `Spatie\MediaLibrary\HasMedia` and is **saved**.
101+
102+
2) Include Livewire & Alpine (usually in your app layout):
103+
104+
```html
105+
@livewireStyles
106+
<style>[x-cloak]{ display:none !important; }</style>
107+
@livewireScripts
108+
```
109+
110+
3) Drop the component into your Blade:
111+
112+
```html
113+
<livewire:media-uploader :for="$user" collection="avatars" preset="images" />
114+
```
115+
116+
---
117+
118+
## Usage Examples
119+
120+
**1) Pass a saved model instance**
121+
```html
122+
<livewire:media-uploader :for="$user" collection="avatars" preset="images" />
123+
```
124+
125+
**2) Short string model + id**
126+
```html
127+
<livewire:media-uploader model="user" :id="$user->id" collection="images" preset="images" />
128+
```
129+
130+
**3) Morph map alias**
131+
```html
132+
<livewire:media-uploader model="users" :id="$user->id" collection="profile" preset="images" />
133+
```
134+
135+
**4) FQCN**
136+
```html
137+
<livewire:media-uploader model="\App\Models\User" :id="$user->id" collection="documents" />
138+
```
139+
140+
**5) Dotted path + custom namespaces**
141+
```html
142+
<livewire:media-uploader
143+
model="crm.contact"
144+
:id="$contactId"
145+
:namespaces="['App\\Domain\\Crm\\Models', 'App\\Models']"
146+
collection="images"
147+
preset="images"
148+
/>
149+
```
150+
151+
**6) Local aliases (per-instance)**
152+
```html
153+
<livewire:media-uploader
154+
model="profile"
155+
:id="$user->id"
156+
:aliases="['profile' => \App\Models\User::class]"
157+
collection="gallery"
158+
/>
159+
```
160+
161+
**7) Single-file mode + hide list**
162+
```html
163+
<livewire:media-uploader
164+
:for="$user"
165+
collection="avatar"
166+
:multiple="false"
167+
:showList="false"
168+
preset="images"
169+
/>
170+
```
171+
172+
**8) Name conflict strategies**
173+
```html
174+
<livewire:media-uploader :for="$user" collection="files" onNameConflict="rename" />
175+
<livewire:media-uploader :for="$user" collection="files" onNameConflict="replace" />
176+
<livewire:media-uploader :for="$user" collection="files" onNameConflict="skip" />
177+
<livewire:media-uploader :for="$user" collection="files" onNameConflict="allow" />
178+
```
179+
180+
**9) Duplicate detection by SHA-256**
181+
```html
182+
<livewire:media-uploader :for="$user" collection="images" preset="images" :skipExactDuplicates="true" />
183+
```
184+
185+
**10) Restrict types/mimes/max size manually**
186+
```html
187+
<livewire:media-uploader
188+
:for="$user"
189+
collection="documents"
190+
:accept="'.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'"
191+
:allowedTypes="['pdf','doc','docx']"
192+
:allowedMimes="['application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document']"
193+
:maxSizeKb="5120"
194+
/>
195+
```
196+
197+
---
198+
199+
## Configuration
200+
201+
The package merges `config/media-uploader.php`:
202+
203+
- `accept_from_config` — if `true`, auto-fills `<input accept>` from the selected preset
204+
- `collections` — map collection name → preset key
205+
- `presets.*.types` — extensions (comma-separated)
206+
- `presets.*.mimes` — MIME types (comma-separated)
207+
- `presets.*.max_kb` — max file size per file in KB
208+
209+
Example:
210+
```php
211+
'collections' => [
212+
'avatars' => 'images',
213+
'images' => 'images',
214+
'attachments' => 'docs',
215+
],
216+
```
217+
218+
The component decides the active preset in this order:
219+
1. Explicit `$preset` prop
220+
2. Mapping from `collections`
221+
3. Fallback to `default`
222+
223+
---
224+
225+
## Props
226+
227+
| Prop | Type | Default | Description |
228+
|---|---|---|---|
229+
| `for` | `Model` || Saved Eloquent model instance implementing `HasMedia`. |
230+
| `model` | `string` || Model resolver: alias, FQCN, morph alias, or dotted path. |
231+
| `id` | `int|string` || Target model id (used with `model`). |
232+
| `collection` | `string` | `images` | Media collection name. |
233+
| `disk` | `?string` | `null` | Storage disk (e.g. `s3`). |
234+
| `multiple` | `bool` | `true` | Toggle multi-file input. |
235+
| `accept` | `?string` | `null` | `<input accept>` override (otherwise may be auto from config). |
236+
| `showList` | `bool` | `true` | Show the attached media list. |
237+
| `maxSizeKb` | `int` | `500` (overridden to preset’s `max_kb` if empty) | Max file size (KB). |
238+
| `preset` | `?string` | `null` | Choose a preset (`images`, `docs`, `videos`, `default`, etc.). |
239+
| `allowedTypes` | `array` | `[]` | Extensions filter (e.g. `['jpg','png']`). |
240+
| `allowedMimes` | `array` | `[]` | MIME filter (e.g. `['image/jpeg']`). |
241+
| `onNameConflict` | `string` | `rename` | Strategy: `rename` \| `replace` \| `skip` \| `allow`. |
242+
| `skipExactDuplicates` | `bool` | `false` | Uses SHA-256 stored in `custom_properties->sha256`. |
243+
| `namespaces` | `array` | `['App\\Models']` | Namespaces for dotted-path resolution. |
244+
| `aliases` | `array` | `[]` | Local alias map, e.g. `['profile' => \App\Models\User::class]`. |
245+
| `attachedFilesTitle` | `string` | `"Current gallery"` | Heading text in the list card. |
246+
247+
---
248+
249+
## Events
250+
251+
The component dispatches browser events you can listen for:
252+
253+
- `media-uploaded` — after an upload completes
254+
- `media-deleted` — after a deletion (`detail.id` contains the Media ID)
255+
- `media-meta-updated` — after saving inline metadata
256+
257+
Example:
258+
```html
259+
<div
260+
x-data
261+
x-on:media-uploaded.window="console.log('uploaded!')"
262+
x-on:media-deleted.window="console.log('deleted', $event.detail?.id)"
263+
>
264+
<livewire:media-uploader :for="$user" collection="images" preset="images" />
265+
</div>
266+
```
267+
268+
---
269+
270+
## Model Setup (Spatie Media Library)
271+
272+
Your model must implement `HasMedia` and be **saved** before attaching media.
273+
274+
```php
275+
use Spatie\MediaLibrary\HasMedia;
276+
use Spatie\MediaLibrary\InteractsWithMedia;
277+
278+
class User extends Model implements HasMedia
279+
{
280+
use InteractsWithMedia;
281+
282+
public function registerMediaCollections(): void
283+
{
284+
$this->addMediaCollection('images');
285+
$this->addMediaCollection('avatars');
286+
}
287+
288+
// Optional thumbnail conversion for the list
289+
public function registerMediaConversions(\Spatie\MediaLibrary\MediaCollections\Models\Media $media = null): void
290+
{
291+
$this->addMediaConversion('thumb')
292+
->fit('contain', 256, 256)
293+
->nonQueued();
294+
}
295+
}
296+
```
297+
298+
> The list view tries `getUrl('thumb')` and falls back to `getUrl()` if no conversion is available.
299+
300+
---
301+
302+
## Overlays & UX Notes
303+
304+
- **Image Preview Overlay** (lightbox): toggled by `x-show="preview.open"`.
305+
- **Delete Confirmation Modal**: toggled by `$wire.confirmingDeleteId !== null`.
306+
- Add once in your layout to prevent flash-of-overlay:
307+
```html
308+
<style>[x-cloak]{ display:none !important; }</style>
309+
```
310+
- Z-index defaults: preview `z-[60]`, delete modal `z-50`. Adjust to your stack if you have higher layers.
311+
312+
---
313+
314+
## Troubleshooting
315+
316+
- **“Target model must be saved…”**
317+
Ensure the model exists in DB (`$model->exists === true`) before rendering the component.
318+
319+
- **“must implement Spatie\MediaLibrary\HasMedia”**
320+
Add `implements HasMedia` + `InteractsWithMedia` to your model.
321+
322+
- **Unknown model class/alias**
323+
If using `model="something"` + `:id`, make sure:
324+
- It’s a valid FQCN, morph alias, or maps via dotted path within `namespaces`, or
325+
- You passed a local alias via `:aliases="['something' => \App\Models\YourModel::class]`.
326+
327+
- **`accept` not applied**
328+
Set `accept_from_config=true` and ensure your preset has `types`/`mimes`. Or override via `accept` prop.
329+
330+
- **No thumbnails**
331+
Add a `thumb` conversion (see [Model Setup](#model-setup-spatie-media-library)).
332+
333+
---
334+
335+
## Roadmap
336+
337+
- Drag-to-reorder (update `order_column`)
338+
- Optional queued conversions hints
339+
340+
PRs welcome!
341+
342+
---
343+
344+
## License
345+
346+
**MIT** © Ray Cuzzart II
347+
348+
---
349+
350+
**Component aliases:** `media-uploader` and `media.media-uploader`
351+
**View namespace:** `media-uploader::livewire.media-uploader`

0 commit comments

Comments
 (0)