10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
458
dev/file-upload/README.md
Normal file
458
dev/file-upload/README.md
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# File Upload mit Livewire Volt + Flux UI
|
||||
|
||||
Vollständige Referenz für den Bild-Upload, wie er in `resources/views/livewire/products/form-teaser.blade.php` implementiert ist.
|
||||
|
||||
---
|
||||
|
||||
## Überblick
|
||||
|
||||
Der Upload nutzt:
|
||||
- **Livewire `WithFileUploads`** – verwaltet temporäre Uploads via signierter URL
|
||||
- **Flux UI `flux:file-upload`** – UI-Komponente (Dropzone + Vorschau)
|
||||
- **Laravel `Storage::disk('public')`** – Permanente Speicherung
|
||||
- **Polymorphe `media`-Tabelle** – Zuordnung von Dateien zu beliebigen Models
|
||||
- **Alpine.js** – Drag-&-Drop-Sortierung der vorhandenen Bilder
|
||||
|
||||
---
|
||||
|
||||
## 1. PHP / Livewire Volt – Komponentenlogik
|
||||
|
||||
### Trait einbinden
|
||||
|
||||
```php
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
new class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $mainImages = [];
|
||||
```
|
||||
|
||||
`WithFileUploads` muss zwingend eingebunden sein. Ohne ihn reagiert `wire:model` nicht auf Datei-Inputs.
|
||||
|
||||
### Validierung
|
||||
|
||||
```php
|
||||
'mainImages' => 'nullable|array|min:0|max:10',
|
||||
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
|
||||
```
|
||||
|
||||
- `mainImages` ist ein **Array** (wegen `multiple`-Upload)
|
||||
- `mainImages.*` validiert jede einzelne Datei
|
||||
- `max:10240` = 10 MB in Kilobyte
|
||||
- Livewire hat intern ein Default-Limit von 12 MB – das greift, bevor die eigene Validierung läuft (Achtung: Wert muss ≤ 12288 KB sein oder `livewire.temporary_file_upload.rules` anpassen)
|
||||
|
||||
### Einzelnes Bild entfernen (Vorschauliste)
|
||||
|
||||
```php
|
||||
public function removePhoto(int $index): void
|
||||
{
|
||||
if (isset($this->mainImages[$index])) {
|
||||
unset($this->mainImages[$index]);
|
||||
$this->mainImages = array_values($this->mainImages);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nach `unset()` unbedingt `array_values()` aufrufen, damit die Array-Indizes wieder bei 0 beginnen – sonst bricht `@foreach` mit `$index` im Template.
|
||||
|
||||
### Vorhandenes Bild aus der DB löschen
|
||||
|
||||
```php
|
||||
public function removeExistingMedia(int $mediaId): void
|
||||
{
|
||||
$media = $this->product->media()->find($mediaId);
|
||||
if ($media) {
|
||||
Storage::disk('public')->delete($media->file_path);
|
||||
$media->delete();
|
||||
$this->existingMedia = collect($this->existingMedia)
|
||||
->reject(fn ($m) => $m['id'] === $mediaId)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Immer erst die **Datei** vom Disk löschen, dann den **DB-Eintrag**. Anschließend `$this->existingMedia` synchronisieren, damit Livewire den State neu rendert.
|
||||
|
||||
### Reihenfolge aktualisieren (Drag & Drop)
|
||||
|
||||
```php
|
||||
public function updateMediaOrder(array $orderedIds): void
|
||||
{
|
||||
foreach ($orderedIds as $position => $mediaId) {
|
||||
$this->product->media()
|
||||
->where('id', $mediaId)
|
||||
->update(['order_column' => $position + 1]);
|
||||
}
|
||||
|
||||
// Lokalen State synchronisieren
|
||||
$this->existingMedia = collect($orderedIds)
|
||||
->map(fn ($id, $index) => collect($this->existingMedia)->firstWhere('id', $id)
|
||||
? array_merge(collect($this->existingMedia)->firstWhere('id', $id), ['order_column' => $index + 1])
|
||||
: null
|
||||
)
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
```
|
||||
|
||||
### Bilder permanent speichern (Neu-Anlage)
|
||||
|
||||
```php
|
||||
$index = 1;
|
||||
foreach ($this->mainImages as $image) {
|
||||
$path = $image->store('products/' . $product->id, 'public');
|
||||
$product->media()->create([
|
||||
'file_path' => $path,
|
||||
'type' => 'image',
|
||||
'alt_text' => $this->name,
|
||||
'order_column' => $index++,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
`$image->store(...)` verschiebt die temporäre Livewire-Datei in den endgültigen Pfad auf dem `public`-Disk.
|
||||
|
||||
### Bilder permanent speichern (Bearbeiten – neue Bilder hinzufügen)
|
||||
|
||||
```php
|
||||
$maxOrder = $this->product->media()->max('order_column') ?? 0;
|
||||
$index = $maxOrder + 1;
|
||||
foreach ($this->mainImages as $image) {
|
||||
$path = $image->store('products/' . $this->product->id, 'public');
|
||||
$this->product->media()->create([
|
||||
'file_path' => $path,
|
||||
'type' => 'image',
|
||||
'alt_text' => $this->name,
|
||||
'order_column' => $index++,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
`order_column` an bestehenden Max-Wert anhängen, nicht von 1 neu beginnen.
|
||||
|
||||
### Nach dem Speichern zurücksetzen
|
||||
|
||||
```php
|
||||
// Neue Bilder leeren
|
||||
$this->mainImages = [];
|
||||
|
||||
// Vorhandene Bilder aus DB neu laden (mit sortBy)
|
||||
$this->existingMedia = $this->product->fresh()->media
|
||||
->sortBy('order_column')
|
||||
->values()
|
||||
->map(fn ($m) => [
|
||||
'id' => $m->id,
|
||||
'file_path' => $m->file_path,
|
||||
'alt_text' => $m->alt_text,
|
||||
'order_column' => $m->order_column,
|
||||
])
|
||||
->toArray();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Blade / Flux UI – Template
|
||||
|
||||
### Upload-Dropzone
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
||||
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Bilder hochladen"
|
||||
text="Nur JPEG oder PNG – max. 10 MB"
|
||||
with-progress />
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
- `wire:model="mainImages"` – bindet an das Array-Property
|
||||
- `multiple` – erlaubt Mehrfachauswahl
|
||||
- `accept` – schränkt den Browser-Dateidialog ein (kein serverseitiger Schutz!)
|
||||
- `with-progress` – zeigt Upload-Fortschrittsbalken
|
||||
|
||||
### Vorschauliste der neu hinzugefügten Bilder
|
||||
|
||||
```blade
|
||||
@if (isset($mainImages) && count($mainImages) > 0)
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
@foreach ($mainImages as $index => $image)
|
||||
<flux:file-item
|
||||
:heading="$image->getClientOriginalName()"
|
||||
:image="(str_starts_with($image->getMimeType() ?? '', 'image/') && $image->isPreviewable())
|
||||
? $image->temporaryUrl()
|
||||
: null"
|
||||
:size="$image->getSize()">
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove
|
||||
wire:click="removePhoto({{ $index }})"
|
||||
aria-label="{{ 'Remove file: ' . $image->getClientOriginalName() }}" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
```
|
||||
|
||||
**Wichtig bei `temporaryUrl()`:**
|
||||
Die Methode gibt nur dann eine URL zurück, wenn die Datei auch vorschaubar ist (`isPreviewable()` prüft die MIME-Type-Whitelist in `config/livewire.php`). Immer beide Bedingungen prüfen, sonst Fehler.
|
||||
|
||||
### Fehleranzeige
|
||||
|
||||
```blade
|
||||
<flux:error name="mainImages" />
|
||||
```
|
||||
|
||||
Zeigt Validierungsfehler für das gesamte Array an (z. B. „Maximal 10 Bilder erlaubt.").
|
||||
Für Fehler auf einzelnen Dateien würde `name="mainImages.0"` etc. verwendet.
|
||||
|
||||
### Drag-&-Drop-Sortierung vorhandener Bilder
|
||||
|
||||
```blade
|
||||
<div x-data="{
|
||||
dragging: null,
|
||||
dragOver: null,
|
||||
items: @js(collect($existingMedia)->pluck('id')->toArray()),
|
||||
onDragStart(e, id) {
|
||||
this.dragging = id;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', id);
|
||||
},
|
||||
onDragOver(e, id) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
this.dragOver = id;
|
||||
},
|
||||
onDrop(e, targetId) {
|
||||
e.preventDefault();
|
||||
if (this.dragging === targetId) { this.dragOver = null; return; }
|
||||
const fromIdx = this.items.indexOf(this.dragging);
|
||||
const toIdx = this.items.indexOf(targetId);
|
||||
this.items.splice(fromIdx, 1);
|
||||
this.items.splice(toIdx, 0, this.dragging);
|
||||
this.dragging = null;
|
||||
this.dragOver = null;
|
||||
$wire.updateMediaOrder(this.items);
|
||||
},
|
||||
onDragEnd() {
|
||||
this.dragging = null;
|
||||
this.dragOver = null;
|
||||
}
|
||||
}" class="flex flex-wrap items-start gap-3">
|
||||
|
||||
@foreach ($existingMedia as $mediaIndex => $media)
|
||||
<div wire:key="existing-media-{{ $media['id'] }}"
|
||||
draggable="true"
|
||||
x-on:dragstart="onDragStart($event, {{ $media['id'] }})"
|
||||
x-on:dragover="onDragOver($event, {{ $media['id'] }})"
|
||||
x-on:drop="onDrop($event, {{ $media['id'] }})"
|
||||
x-on:dragend="onDragEnd()"
|
||||
:class="{
|
||||
'opacity-50 scale-95': dragging === {{ $media['id'] }},
|
||||
'ring-2 ring-blue-400 ring-offset-2': dragOver === {{ $media['id'] }} && dragging !== {{ $media['id'] }}
|
||||
}"
|
||||
class="group relative cursor-grab active:cursor-grabbing transition-all duration-150">
|
||||
|
||||
@if ($mediaIndex === 0)
|
||||
<div class="absolute -top-1 -left-1 z-10 bg-blue-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-md shadow">
|
||||
Standard
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<img src="{{ Storage::url($media['file_path']) }}"
|
||||
alt="{{ $media['alt_text'] ?? '' }}"
|
||||
class="h-24 w-24 rounded-lg object-cover border border-zinc-200 dark:border-zinc-700
|
||||
{{ $mediaIndex === 0 ? 'ring-2 ring-blue-500' : '' }}" />
|
||||
|
||||
<flux:button
|
||||
wire:click="removeExistingMedia({{ $media['id'] }})"
|
||||
wire:confirm="Bild wirklich löschen?"
|
||||
variant="filled" size="xs" icon="trash"
|
||||
class="absolute -top-2 -right-2 !bg-red-500 !text-white hover:!bg-red-600" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
```
|
||||
|
||||
**Wichtig:** `wire:key="existing-media-{{ $media['id'] }}"` ist zwingend, damit Livewire beim Re-Render die DOM-Elemente korrekt zuordnet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenbank – Media-Tabelle
|
||||
|
||||
```php
|
||||
// Migration: database/migrations/xxxx_create_media_table.php
|
||||
|
||||
Schema::create('media', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('model_type'); // z. B. "App\Models\Product"
|
||||
$table->unsignedBigInteger('model_id');
|
||||
$table->string('file_path'); // relativer Pfad auf dem public-Disk
|
||||
$table->string('type')->default('image'); // 'image', 'video', 'pdf', '3d_model'
|
||||
$table->string('alt_text')->nullable();
|
||||
$table->integer('order_column')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['model_type', 'model_id']);
|
||||
});
|
||||
```
|
||||
|
||||
### Media-Model (`app/Models/Media.php`)
|
||||
|
||||
```php
|
||||
class Media extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'model_type', 'model_id', 'file_path', 'type', 'alt_text', 'order_column',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['order_column' => 'integer'];
|
||||
}
|
||||
|
||||
/** Polymorphe Beziehung zum Eltern-Model */
|
||||
public function model(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beziehung im Parent-Model (`app/Models/Product.php`)
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
public function media(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Media::class, 'model');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Filesystem-Konfiguration (`config/filesystems.php`)
|
||||
|
||||
```php
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
```
|
||||
|
||||
### Symlink anlegen
|
||||
|
||||
```bash
|
||||
php artisan storage:link
|
||||
```
|
||||
|
||||
Erstellt `public/storage` → `storage/app/public`. **Ohne diesen Symlink sind hochgeladene Bilder nicht über den Browser erreichbar.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Kritische System-Anpassungen
|
||||
|
||||
### 5a. `bootstrap/app.php` – Reverse-Proxy / HTTPS
|
||||
|
||||
```php
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
// Traefik/nginx Proxy: X-Forwarded-Proto-Header vertrauen
|
||||
// ZWINGEND für korrekte signierte Upload-URLs hinter einem HTTPS-Proxy
|
||||
$middleware->trustProxies(at: '*');
|
||||
})
|
||||
```
|
||||
|
||||
**Warum?**
|
||||
Livewire generiert für temporäre Uploads **signierte URLs**. Wenn die App hinter einem Reverse-Proxy (Traefik, nginx, Load Balancer) läuft und der Proxy HTTPS terminiert, glaubt Laravel intern, es sei HTTP. Die signierte URL wird dann mit `http://` generiert, der Browser sendet aber `https://` – die Signatur stimmt nicht, Upload schlägt fehl mit `403`.
|
||||
|
||||
### 5b. `app/Providers/AppServiceProvider.php` – Schema erzwingen
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
// X-Forwarded-Proto auswerten und Schema erzwingen
|
||||
// Nötig für Livewire Upload-URLs hinter Traefik
|
||||
$scheme = request()->header('X-Forwarded-Proto')
|
||||
?? request()->server('HTTP_X_FORWARDED_PROTO')
|
||||
?? (request()->secure() ? 'https' : 'http');
|
||||
|
||||
if ($scheme === 'https') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Warum zusätzlich zum `trustProxies`?**
|
||||
`trustProxies` reicht in manchen Proxy-Setups nicht aus, wenn der Header-Name variiert. `URL::forceScheme('https')` ist die sichere Ergänzung, die sicherstellt, dass alle generierten URLs das korrekte Schema haben.
|
||||
|
||||
**Ohne diese beiden Maßnahmen** scheitert der Upload mit einer `403 Signature mismatch`-Fehlermeldung in der Browser-Console – besonders frustrierend, weil kein PHP-Fehler erscheint.
|
||||
|
||||
---
|
||||
|
||||
## 6. Livewire-Konfiguration (`config/livewire.php`)
|
||||
|
||||
```php
|
||||
'temporary_file_upload' => [
|
||||
'disk' => null, // null = default-Disk (meist 'local')
|
||||
'rules' => null, // null = ['required', 'file', 'max:12288'] (12 MB Default)
|
||||
'directory' => null, // null = 'livewire-tmp'
|
||||
'middleware' => null, // null = 'throttle:60,1'
|
||||
'preview_mimes' => [ // Diese MIME-Types erlauben temporaryUrl()
|
||||
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp',
|
||||
'pdf', 'mp4', 'mov', 'avi', 'wmv', 'mp3', ...
|
||||
],
|
||||
'max_upload_time' => 5, // Minuten bis Upload ungültig wird
|
||||
'cleanup' => true, // Tmp-Dateien > 24h automatisch löschen
|
||||
],
|
||||
```
|
||||
|
||||
**Wichtig:** Das interne Default-Limit ist **12 MB** (`max:12288`). Eigene Validierungsregeln wie `max:10240` müssen immer unter diesem Wert liegen. Soll das Limit erhöht werden, muss `rules` hier überschrieben werden.
|
||||
|
||||
---
|
||||
|
||||
## 7. Checkliste für ein neues Projekt
|
||||
|
||||
| Schritt | Was | Wo |
|
||||
|---------|-----|----|
|
||||
| ✅ | `use WithFileUploads` im Volt/Livewire-Component | Komponentenklasse |
|
||||
| ✅ | `public array $images = []` Property anlegen | Komponentenklasse |
|
||||
| ✅ | `'images.*' => 'mimes:jpeg,png\|max:10240'` Validierung | `save()`-Methode |
|
||||
| ✅ | `$image->store('pfad', 'public')` beim Speichern | `save()`-Methode |
|
||||
| ✅ | `$this->images = []` nach dem Speichern leeren | `save()`-Methode |
|
||||
| ✅ | `php artisan storage:link` ausführen | Terminal / Deploy |
|
||||
| ✅ | `$middleware->trustProxies(at: '*')` | `bootstrap/app.php` |
|
||||
| ✅ | `URL::forceScheme('https')` bei HTTPS-Proxy | `AppServiceProvider.php` |
|
||||
| ✅ | `wire:key` in Foreach-Schleifen | Blade-Template |
|
||||
| ✅ | `array_values()` nach `unset()` auf dem Array | `removePhoto()` |
|
||||
| ✅ | `isPreviewable()` vor `temporaryUrl()` prüfen | Blade-Template |
|
||||
|
||||
---
|
||||
|
||||
## 8. Häufige Fallstricke
|
||||
|
||||
### Upload schlägt fehl mit 403 (Signature mismatch)
|
||||
→ Reverse-Proxy-Problem. Siehe Punkt 5a und 5b.
|
||||
|
||||
### Vorschau-Thumbnail zeigt nichts an
|
||||
→ `isPreviewable()` gibt `false` zurück, wenn der MIME-Type nicht in `preview_mimes` steht. In der Livewire-Config prüfen.
|
||||
|
||||
### Nach `removePhoto()` stimmen die Indizes nicht
|
||||
→ `array_values()` vergessen. Livewire sendet den Index als Parameter – ohne Reindizierung kommt es zu Off-by-One-Fehlern.
|
||||
|
||||
### Upload-Limit-Fehler vor der Validierung
|
||||
→ PHP `upload_max_filesize` und `post_max_size` in `php.ini` überprüfen. Auch Livewires internes `max:12288`-Limit beachten.
|
||||
|
||||
### `temporaryUrl()` wirft eine Exception
|
||||
→ Bei lokalen Disks ohne `serve: true` in `filesystems.php` funktioniert `temporaryUrl()` nicht. Entweder `serve: true` setzen oder S3 verwenden. Im Template immer mit `isPreviewable()` absichern.
|
||||
|
||||
### Bilder nach Deploy nicht sichtbar
|
||||
→ `php artisan storage:link` auf dem Produktionssystem ausführen. Im Docker-Container nach jedem `down/up` prüfen, ob der Symlink noch existiert.
|
||||
302
dev/file-upload/cms/CabinetDisplay.php
Normal file
302
dev/file-upload/cms/CabinetDisplay.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\DisplayFooterContent;
|
||||
use App\Models\DisplayVideo;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetDisplay extends Component
|
||||
{
|
||||
// Video-Verwaltung
|
||||
public $videoId = null;
|
||||
|
||||
public $videoFilename = '';
|
||||
|
||||
public $videoTitle = '';
|
||||
|
||||
public $videoPosition = 25;
|
||||
|
||||
public $videoIsActive = true;
|
||||
|
||||
public $showVideoModal = false;
|
||||
|
||||
public $availableVideos = [];
|
||||
|
||||
// Footer-Content-Verwaltung
|
||||
public $footerId = null;
|
||||
|
||||
public $footerHeadline = '';
|
||||
|
||||
public $footerSubline = '';
|
||||
|
||||
public $footerUrl = '';
|
||||
|
||||
public $footerIsActive = true;
|
||||
|
||||
public $showFooterModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
|
||||
*/
|
||||
public function loadAvailableVideos()
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$files = File::files($assetsPath);
|
||||
$this->availableVideos = collect($files)
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VIDEO-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openVideoModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$this->videoId = $video->id;
|
||||
$this->videoFilename = $video->filename;
|
||||
$this->videoTitle = $video->title ?? '';
|
||||
$this->videoPosition = $video->position;
|
||||
$this->videoIsActive = $video->is_active;
|
||||
} else {
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
$this->showVideoModal = true;
|
||||
}
|
||||
|
||||
public function saveVideo()
|
||||
{
|
||||
$this->validate([
|
||||
'videoFilename' => 'required|string',
|
||||
'videoPosition' => 'required|integer|min:0|max:100',
|
||||
], [
|
||||
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
|
||||
'videoPosition.required' => 'Die Position ist erforderlich.',
|
||||
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
'is_active' => $this->videoIsActive,
|
||||
];
|
||||
|
||||
if ($this->videoId) {
|
||||
$video = DisplayVideo::findOrFail($this->videoId);
|
||||
$video->update($data);
|
||||
session()->flash('success', 'Video erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
DisplayVideo::create($data);
|
||||
session()->flash('success', 'Video erfolgreich hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeVideoModal();
|
||||
}
|
||||
|
||||
public function deleteVideo($id)
|
||||
{
|
||||
DisplayVideo::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Video erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleVideoStatus($id)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$video->update(['is_active' => ! $video->is_active]);
|
||||
}
|
||||
|
||||
public function moveVideo($id, $direction)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$currentOrder = $video->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetVideoForm()
|
||||
{
|
||||
$this->videoId = null;
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
public function closeVideoModal()
|
||||
{
|
||||
$this->showVideoModal = false;
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOTER-CONTENT-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openFooterModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$this->footerId = $footer->id;
|
||||
$this->footerHeadline = $footer->headline;
|
||||
$this->footerSubline = $footer->subline;
|
||||
$this->footerUrl = $footer->url;
|
||||
$this->footerIsActive = $footer->is_active;
|
||||
} else {
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
$this->showFooterModal = true;
|
||||
}
|
||||
|
||||
public function saveFooter()
|
||||
{
|
||||
$this->validate([
|
||||
'footerHeadline' => 'required|string|max:255',
|
||||
'footerSubline' => 'required|string|max:255',
|
||||
'footerUrl' => 'nullable|url',
|
||||
], [
|
||||
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
|
||||
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
|
||||
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
'is_active' => $this->footerIsActive,
|
||||
];
|
||||
|
||||
if ($this->footerId) {
|
||||
$footer = DisplayFooterContent::findOrFail($this->footerId);
|
||||
$footer->update($data);
|
||||
|
||||
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
|
||||
if ($footer->url && ! $footer->short_code) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
|
||||
$footer = DisplayFooterContent::create($data);
|
||||
|
||||
// Short-Code nur generieren wenn URL vorhanden
|
||||
if ($footer->url) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: '.$footer->short_url);
|
||||
} else {
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
|
||||
}
|
||||
}
|
||||
|
||||
$this->closeFooterModal();
|
||||
}
|
||||
|
||||
public function regenerateShortCode($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Short-Code wurde neu generiert!');
|
||||
}
|
||||
|
||||
public function resetClicks($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->clicks = 0;
|
||||
$footer->save();
|
||||
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function deleteFooter($id)
|
||||
{
|
||||
DisplayFooterContent::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleFooterStatus($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->update(['is_active' => ! $footer->is_active]);
|
||||
}
|
||||
|
||||
public function moveFooter($id, $direction)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$currentOrder = $footer->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFooterForm()
|
||||
{
|
||||
$this->footerId = null;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
public function closeFooterModal()
|
||||
{
|
||||
$this->showFooterModal = false;
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$videos = DisplayVideo::orderBy('sort_order')->get();
|
||||
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
|
||||
|
||||
return view('livewire.admin.cms.cabinet-display', [
|
||||
'videos' => $videos,
|
||||
'footerContents' => $footerContents,
|
||||
]);
|
||||
}
|
||||
}
|
||||
189
dev/file-upload/cms/CabinetInfoTablet.php
Normal file
189
dev/file-upload/cms/CabinetInfoTablet.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\CabinetTabletSetting;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetInfoTablet extends Component
|
||||
{
|
||||
// Store status mode
|
||||
public string $storeStatus = 'auto';
|
||||
|
||||
public string $noticeHeadline = '';
|
||||
|
||||
public string $noticeSubtext = '';
|
||||
|
||||
// Override times for today
|
||||
public ?string $overrideOpenToday = '';
|
||||
|
||||
public ?string $overrideCloseToday = '';
|
||||
|
||||
// Appointment
|
||||
public ?string $nextAppointmentDate = null;
|
||||
|
||||
public ?string $nextAppointmentTime = '';
|
||||
|
||||
// Structured opening hours per weekday (open + close, empty = closed)
|
||||
public ?string $hoursMondayOpen = '10:00';
|
||||
|
||||
public ?string $hoursMondayClose = '18:00';
|
||||
|
||||
public ?string $hoursTuesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursTuesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursWednesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursWednesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursThursdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursThursdayClose = '18:00';
|
||||
|
||||
public ?string $hoursFridayOpen = '10:00';
|
||||
|
||||
public ?string $hoursFridayClose = '18:00';
|
||||
|
||||
public ?string $hoursSaturdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursSaturdayClose = '14:00';
|
||||
|
||||
public ?string $hoursSundayOpen = '';
|
||||
|
||||
public ?string $hoursSundayClose = '';
|
||||
|
||||
// Contact
|
||||
public string $contactPhone = '';
|
||||
|
||||
public string $contactEmail = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$s = CabinetTabletSetting::current();
|
||||
|
||||
$this->storeStatus = $s->store_status ?? 'auto';
|
||||
$this->noticeHeadline = $s->notice_headline ?? '';
|
||||
$this->noticeSubtext = $s->notice_subtext ?? '';
|
||||
$this->overrideOpenToday = $s->override_open_today ?? '';
|
||||
$this->overrideCloseToday = $s->override_close_today ?? '';
|
||||
$this->nextAppointmentDate = $s->next_appointment_date?->format('Y-m-d');
|
||||
$this->nextAppointmentTime = $s->next_appointment_time ?? '';
|
||||
$this->hoursMondayOpen = $s->hours_monday_open ?? '';
|
||||
$this->hoursMondayClose = $s->hours_monday_close ?? '';
|
||||
$this->hoursTuesdayOpen = $s->hours_tuesday_open ?? '';
|
||||
$this->hoursTuesdayClose = $s->hours_tuesday_close ?? '';
|
||||
$this->hoursWednesdayOpen = $s->hours_wednesday_open ?? '';
|
||||
$this->hoursWednesdayClose = $s->hours_wednesday_close ?? '';
|
||||
$this->hoursThursdayOpen = $s->hours_thursday_open ?? '';
|
||||
$this->hoursThursdayClose = $s->hours_thursday_close ?? '';
|
||||
$this->hoursFridayOpen = $s->hours_friday_open ?? '';
|
||||
$this->hoursFridayClose = $s->hours_friday_close ?? '';
|
||||
$this->hoursSaturdayOpen = $s->hours_saturday_open ?? '';
|
||||
$this->hoursSaturdayClose = $s->hours_saturday_close ?? '';
|
||||
$this->hoursSundayOpen = $s->hours_sunday_open ?? '';
|
||||
$this->hoursSundayClose = $s->hours_sunday_close ?? '';
|
||||
$this->contactPhone = $s->contact_phone ?? '';
|
||||
$this->contactEmail = $s->contact_email ?? '';
|
||||
}
|
||||
|
||||
private function timeRule(): array
|
||||
{
|
||||
return ['nullable', 'string', 'regex:/^(\d{2}:\d{2})?$/'];
|
||||
}
|
||||
|
||||
private function toNullIfEmpty(?string $value): ?string
|
||||
{
|
||||
return $value !== null && trim($value) !== '' ? trim($value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $hours Optional: Time picker values from DOM (bypasses wire:model sync issues)
|
||||
*/
|
||||
public function save(array $hours = []): void
|
||||
{
|
||||
foreach ($hours as $prop => $value) {
|
||||
if (property_exists($this, $prop)) {
|
||||
$this->{$prop} = $value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
$timeRule = $this->timeRule();
|
||||
|
||||
$this->validate([
|
||||
'storeStatus' => 'required|in:auto,notice,warning,closed',
|
||||
'noticeHeadline' => 'nullable|string|max:40',
|
||||
'noticeSubtext' => 'nullable|string|max:80',
|
||||
'overrideOpenToday' => $timeRule,
|
||||
'overrideCloseToday' => $timeRule,
|
||||
'nextAppointmentDate' => 'nullable|date',
|
||||
'nextAppointmentTime' => $timeRule,
|
||||
'hoursMondayOpen' => $timeRule,
|
||||
'hoursMondayClose' => $timeRule,
|
||||
'hoursTuesdayOpen' => $timeRule,
|
||||
'hoursTuesdayClose' => $timeRule,
|
||||
'hoursWednesdayOpen' => $timeRule,
|
||||
'hoursWednesdayClose' => $timeRule,
|
||||
'hoursThursdayOpen' => $timeRule,
|
||||
'hoursThursdayClose' => $timeRule,
|
||||
'hoursFridayOpen' => $timeRule,
|
||||
'hoursFridayClose' => $timeRule,
|
||||
'hoursSaturdayOpen' => $timeRule,
|
||||
'hoursSaturdayClose' => $timeRule,
|
||||
'hoursSundayOpen' => $timeRule,
|
||||
'hoursSundayClose' => $timeRule,
|
||||
'contactPhone' => 'nullable|string|max:50',
|
||||
'contactEmail' => 'nullable|email|max:100',
|
||||
], [
|
||||
'storeStatus.required' => 'Der Store-Status ist erforderlich.',
|
||||
'storeStatus.in' => 'Ungültiger Status. Erlaubt: auto, notice, warning, closed.',
|
||||
'noticeHeadline.max' => 'Die Headline darf maximal 40 Zeichen haben.',
|
||||
'noticeSubtext.max' => 'Der Subtext darf maximal 80 Zeichen haben.',
|
||||
'overrideOpenToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'overrideCloseToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'nextAppointmentTime.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'contactEmail.email' => 'Bitte eine gültige E-Mail-Adresse eingeben.',
|
||||
]);
|
||||
CabinetTabletSetting::current()->update([
|
||||
'store_status' => $this->storeStatus,
|
||||
'notice_headline' => $this->toNullIfEmpty($this->noticeHeadline),
|
||||
'notice_subtext' => $this->toNullIfEmpty($this->noticeSubtext),
|
||||
'override_open_today' => $this->toNullIfEmpty($this->overrideOpenToday),
|
||||
'override_close_today' => $this->toNullIfEmpty($this->overrideCloseToday),
|
||||
'next_appointment_date' => $this->toNullIfEmpty($this->nextAppointmentDate),
|
||||
'next_appointment_time' => $this->toNullIfEmpty($this->nextAppointmentTime),
|
||||
'hours_monday_open' => $this->toNullIfEmpty($this->hoursMondayOpen),
|
||||
'hours_monday_close' => $this->toNullIfEmpty($this->hoursMondayClose),
|
||||
'hours_tuesday_open' => $this->toNullIfEmpty($this->hoursTuesdayOpen),
|
||||
'hours_tuesday_close' => $this->toNullIfEmpty($this->hoursTuesdayClose),
|
||||
'hours_wednesday_open' => $this->toNullIfEmpty($this->hoursWednesdayOpen),
|
||||
'hours_wednesday_close' => $this->toNullIfEmpty($this->hoursWednesdayClose),
|
||||
'hours_thursday_open' => $this->toNullIfEmpty($this->hoursThursdayOpen),
|
||||
'hours_thursday_close' => $this->toNullIfEmpty($this->hoursThursdayClose),
|
||||
'hours_friday_open' => $this->toNullIfEmpty($this->hoursFridayOpen),
|
||||
'hours_friday_close' => $this->toNullIfEmpty($this->hoursFridayClose),
|
||||
'hours_saturday_open' => $this->toNullIfEmpty($this->hoursSaturdayOpen),
|
||||
'hours_saturday_close' => $this->toNullIfEmpty($this->hoursSaturdayClose),
|
||||
'hours_sunday_open' => $this->toNullIfEmpty($this->hoursSundayOpen),
|
||||
'hours_sunday_close' => $this->toNullIfEmpty($this->hoursSundayClose),
|
||||
'contact_phone' => $this->toNullIfEmpty($this->contactPhone),
|
||||
'contact_email' => $this->toNullIfEmpty($this->contactEmail),
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Info-Tablet Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
public function clearOverrides(): void
|
||||
{
|
||||
CabinetTabletSetting::current()->clearOverrides();
|
||||
$this->overrideOpenToday = '';
|
||||
$this->overrideCloseToday = '';
|
||||
|
||||
session()->flash('success', 'Sonderöffnungszeiten wurden zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\View\View
|
||||
{
|
||||
return view('livewire.admin.cms.cabinet-info-tablet');
|
||||
}
|
||||
}
|
||||
153
dev/file-upload/cms/DisplayList.php
Normal file
153
dev/file-upload/cms/DisplayList.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayList extends Component
|
||||
{
|
||||
public $showModal = false;
|
||||
|
||||
public $displayId = null;
|
||||
|
||||
public $displayName = '';
|
||||
|
||||
public $displayLocation = '';
|
||||
|
||||
/** @var array<int> */
|
||||
public $selectedVersionIds = [];
|
||||
|
||||
public $displayIsActive = true;
|
||||
|
||||
public $addVersionSelect = null;
|
||||
|
||||
public function openModal(?int $id = null): void
|
||||
{
|
||||
if ($id) {
|
||||
$display = Display::with('versions')->findOrFail($id);
|
||||
$this->displayId = $display->id;
|
||||
$this->displayName = $display->name;
|
||||
$this->displayLocation = $display->location ?? '';
|
||||
$this->selectedVersionIds = $display->versions->pluck('id')->toArray();
|
||||
$this->displayIsActive = $display->is_active;
|
||||
} else {
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function addVersion(?int $versionId = null): void
|
||||
{
|
||||
$id = $versionId ?? $this->addVersionSelect;
|
||||
|
||||
if ($id && ! in_array((int) $id, $this->selectedVersionIds)) {
|
||||
$this->selectedVersionIds[] = (int) $id;
|
||||
}
|
||||
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function removeVersion(int $index): void
|
||||
{
|
||||
array_splice($this->selectedVersionIds, $index, 1);
|
||||
}
|
||||
|
||||
public function moveVersion(int $index, string $direction): void
|
||||
{
|
||||
$newIndex = $direction === 'up' ? $index - 1 : $index + 1;
|
||||
|
||||
if ($newIndex < 0 || $newIndex >= count($this->selectedVersionIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$temp = $this->selectedVersionIds[$index];
|
||||
$this->selectedVersionIds[$index] = $this->selectedVersionIds[$newIndex];
|
||||
$this->selectedVersionIds[$newIndex] = $temp;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'displayName' => 'required|string|max:255',
|
||||
'displayLocation' => 'nullable|string|max:255',
|
||||
'selectedVersionIds' => 'array',
|
||||
'selectedVersionIds.*' => 'exists:display_versions,id',
|
||||
], [
|
||||
'displayName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'name' => $this->displayName,
|
||||
'location' => $this->displayLocation ?: null,
|
||||
'is_active' => $this->displayIsActive,
|
||||
];
|
||||
|
||||
if ($this->displayId) {
|
||||
$display = Display::findOrFail($this->displayId);
|
||||
$display->update($data);
|
||||
session()->flash('success', 'Display erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$display = Display::create($data);
|
||||
session()->flash('success', 'Display erfolgreich erstellt!');
|
||||
}
|
||||
|
||||
// Sync versions with sort_order
|
||||
$syncData = [];
|
||||
foreach ($this->selectedVersionIds as $sortOrder => $versionId) {
|
||||
$syncData[$versionId] = ['sort_order' => $sortOrder];
|
||||
}
|
||||
$display->versions()->sync($syncData);
|
||||
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
public function deleteDisplay(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$name = $display->name;
|
||||
$display->delete();
|
||||
|
||||
session()->flash('success', 'Display "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$display->update(['is_active' => ! $display->is_active]);
|
||||
}
|
||||
|
||||
public function closeModal(): void
|
||||
{
|
||||
$this->showModal = false;
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->displayId = null;
|
||||
$this->displayName = '';
|
||||
$this->displayLocation = '';
|
||||
$this->selectedVersionIds = [];
|
||||
$this->displayIsActive = true;
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$displays = Display::with('versions')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$versions = DisplayVersion::active()
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-list', [
|
||||
'displays' => $displays,
|
||||
'versions' => $versions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
437
dev/file-upload/cms/DisplayVersionEditor.php
Normal file
437
dev/file-upload/cms/DisplayVersionEditor.php
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionEditor extends Component
|
||||
{
|
||||
public DisplayVersion $version;
|
||||
|
||||
public string $versionName = '';
|
||||
|
||||
// Item Modal
|
||||
public bool $showItemModal = false;
|
||||
|
||||
public ?int $itemId = null;
|
||||
|
||||
public string $itemType = '';
|
||||
|
||||
// Video-Display: Video fields
|
||||
public string $videoFilename = '';
|
||||
|
||||
public string $videoTitle = '';
|
||||
|
||||
public int $videoPosition = 25;
|
||||
|
||||
public bool $videoIsActive = true;
|
||||
|
||||
// Video-Display: Footer fields
|
||||
public string $footerHeadline = '';
|
||||
|
||||
public string $footerSubline = '';
|
||||
|
||||
public string $footerUrl = '';
|
||||
|
||||
public bool $footerIsActive = true;
|
||||
|
||||
// B2in: Media fields
|
||||
public string $mediaType = 'image';
|
||||
|
||||
public string $mediaCategory = 'immobilien';
|
||||
|
||||
public string $mediaUrl = '';
|
||||
|
||||
public string $mediaHeadline = '';
|
||||
|
||||
public string $mediaSubline = '';
|
||||
|
||||
public int $mediaDuration = 10;
|
||||
|
||||
public bool $mediaIsActive = true;
|
||||
|
||||
// Offers: Slide fields
|
||||
public string $slideType = 'product-hero';
|
||||
|
||||
public int $slideDuration = 8000;
|
||||
|
||||
public string $slideImageUrl = '';
|
||||
|
||||
public string $slideBadge = '';
|
||||
|
||||
public string $slideEyebrow = '';
|
||||
|
||||
public string $slideTitle = '';
|
||||
|
||||
public string $slideSubline = '';
|
||||
|
||||
public string $slidePrice = '';
|
||||
|
||||
public string $slideOriginalPrice = '';
|
||||
|
||||
public string $slideTagText = '';
|
||||
|
||||
/** @var array<string> */
|
||||
public array $slideBullets = [];
|
||||
|
||||
public string $slideDisclaimer = '';
|
||||
|
||||
public string $slideQrUrl = '';
|
||||
|
||||
public string $slideQrTitle = '';
|
||||
|
||||
public string $slideContact = '';
|
||||
|
||||
public bool $slideShowBrandText = false;
|
||||
|
||||
public string $slideBrandTagline = '';
|
||||
|
||||
public bool $slideIsActive = true;
|
||||
|
||||
// Settings Modal
|
||||
public bool $showSettingsModal = false;
|
||||
|
||||
public array $settings = [];
|
||||
|
||||
/** @var array<string> */
|
||||
public array $availableVideos = [];
|
||||
|
||||
public function mount(DisplayVersion $displayVersion): void
|
||||
{
|
||||
$this->version = $displayVersion;
|
||||
$this->versionName = $displayVersion->name;
|
||||
$this->settings = $displayVersion->settings ?? [];
|
||||
|
||||
if ($this->version->type === DisplayVersionType::VideoDisplay) {
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadAvailableVideos(): void
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$this->availableVideos = collect(File::files($assetsPath))
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTheme(): void
|
||||
{
|
||||
$settings = $this->version->settings ?? [];
|
||||
$settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark';
|
||||
$this->version->update(['settings' => $settings]);
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function saveName(): void
|
||||
{
|
||||
$this->validate([
|
||||
'versionName' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$this->version->update(['name' => $this->versionName]);
|
||||
session()->flash('success', 'Name aktualisiert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SETTINGS
|
||||
// ========================================
|
||||
|
||||
public function openSettingsModal(): void
|
||||
{
|
||||
$this->settings = $this->version->settings ?? [];
|
||||
$this->showSettingsModal = true;
|
||||
}
|
||||
|
||||
public function saveSettings(): void
|
||||
{
|
||||
$this->version->update(['settings' => $this->settings]);
|
||||
$this->showSettingsModal = false;
|
||||
session()->flash('success', 'Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ITEM CRUD
|
||||
// ========================================
|
||||
|
||||
public function openItemModal(?int $id = null, string $type = ''): void
|
||||
{
|
||||
$this->resetItemForm();
|
||||
|
||||
if ($id) {
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$this->itemId = $item->id;
|
||||
$this->itemType = $item->item_type;
|
||||
$this->loadItemContent($item);
|
||||
} else {
|
||||
$this->itemType = $type ?: $this->defaultItemType();
|
||||
}
|
||||
|
||||
$this->showItemModal = true;
|
||||
}
|
||||
|
||||
public function saveItem(): void
|
||||
{
|
||||
$content = $this->buildItemContent();
|
||||
$isActive = $this->getActiveFlag();
|
||||
|
||||
if ($this->itemId) {
|
||||
$item = DisplayVersionItem::findOrFail($this->itemId);
|
||||
$item->update([
|
||||
'content' => $content,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt aktualisiert!');
|
||||
} else {
|
||||
$maxSort = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $this->itemType)
|
||||
->max('sort_order') ?? -1;
|
||||
|
||||
DisplayVersionItem::create([
|
||||
'display_version_id' => $this->version->id,
|
||||
'item_type' => $this->itemType,
|
||||
'content' => $content,
|
||||
'sort_order' => $maxSort + 1,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeItemModal();
|
||||
}
|
||||
|
||||
public function deleteItem(int $id): void
|
||||
{
|
||||
DisplayVersionItem::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Inhalt gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleItemStatus(int $id): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$item->update(['is_active' => ! $item->is_active]);
|
||||
}
|
||||
|
||||
public function moveItem(int $id, string $direction): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$currentOrder = $item->sort_order;
|
||||
|
||||
$swapItem = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $item->item_type)
|
||||
->where('sort_order', $direction === 'up' ? $currentOrder - 1 : $currentOrder + 1)
|
||||
->first();
|
||||
|
||||
if ($swapItem) {
|
||||
$item->update(['sort_order' => $swapItem->sort_order]);
|
||||
$swapItem->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addBullet(): void
|
||||
{
|
||||
$this->slideBullets[] = '';
|
||||
}
|
||||
|
||||
public function removeBullet(int $index): void
|
||||
{
|
||||
unset($this->slideBullets[$index]);
|
||||
$this->slideBullets = array_values($this->slideBullets);
|
||||
}
|
||||
|
||||
public function closeItemModal(): void
|
||||
{
|
||||
$this->showItemModal = false;
|
||||
$this->resetItemForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPERS
|
||||
// ========================================
|
||||
|
||||
private function loadItemContent(DisplayVersionItem $item): void
|
||||
{
|
||||
$content = $item->content;
|
||||
|
||||
match ($item->item_type) {
|
||||
'video' => $this->loadVideoContent($content),
|
||||
'footer' => $this->loadFooterContent($content),
|
||||
'media' => $this->loadMediaContent($content),
|
||||
'slide' => $this->loadSlideContent($content),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function loadVideoContent(array $content): void
|
||||
{
|
||||
$this->videoFilename = $content['filename'] ?? '';
|
||||
$this->videoTitle = $content['title'] ?? '';
|
||||
$this->videoPosition = $content['position'] ?? 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
private function loadFooterContent(array $content): void
|
||||
{
|
||||
$this->footerHeadline = $content['headline'] ?? '';
|
||||
$this->footerSubline = $content['subline'] ?? '';
|
||||
$this->footerUrl = $content['url'] ?? '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
private function loadMediaContent(array $content): void
|
||||
{
|
||||
$this->mediaType = $content['media_type'] ?? 'image';
|
||||
$this->mediaCategory = $content['category'] ?? 'immobilien';
|
||||
$this->mediaUrl = $content['media_url'] ?? '';
|
||||
$this->mediaHeadline = $content['headline'] ?? '';
|
||||
$this->mediaSubline = $content['subline'] ?? '';
|
||||
$this->mediaDuration = $content['duration_seconds'] ?? 10;
|
||||
$this->mediaIsActive = true;
|
||||
}
|
||||
|
||||
private function loadSlideContent(array $content): void
|
||||
{
|
||||
$this->slideType = $content['type'] ?? 'product-hero';
|
||||
$this->slideDuration = $content['duration'] ?? 8000;
|
||||
$this->slideImageUrl = $content['image_url'] ?? '';
|
||||
$this->slideBadge = $content['badge_text'] ?? '';
|
||||
$this->slideEyebrow = $content['eyebrow'] ?? '';
|
||||
$this->slideTitle = $content['title'] ?? '';
|
||||
$this->slideSubline = $content['subline'] ?? '';
|
||||
$this->slidePrice = $content['price'] ?? '';
|
||||
$this->slideOriginalPrice = $content['original_price'] ?? '';
|
||||
$this->slideTagText = $content['tag_text'] ?? '';
|
||||
$this->slideBullets = $content['bullets'] ?? [];
|
||||
$this->slideDisclaimer = $content['disclaimer'] ?? '';
|
||||
$this->slideQrUrl = $content['qr_url'] ?? '';
|
||||
$this->slideQrTitle = $content['qr_title'] ?? '';
|
||||
$this->slideContact = $content['contact'] ?? '';
|
||||
$this->slideShowBrandText = $content['show_brand_text'] ?? false;
|
||||
$this->slideBrandTagline = $content['brand_tagline'] ?? '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildItemContent(): array
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
],
|
||||
'footer' => [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
],
|
||||
'media' => [
|
||||
'media_type' => $this->mediaType,
|
||||
'category' => $this->mediaCategory,
|
||||
'media_url' => $this->mediaUrl,
|
||||
'headline' => $this->mediaHeadline,
|
||||
'subline' => $this->mediaSubline,
|
||||
'duration_seconds' => $this->mediaDuration,
|
||||
],
|
||||
'slide' => [
|
||||
'type' => $this->slideType,
|
||||
'duration' => $this->slideDuration,
|
||||
'image_url' => $this->slideImageUrl,
|
||||
'badge_text' => $this->slideBadge,
|
||||
'eyebrow' => $this->slideEyebrow,
|
||||
'title' => $this->slideTitle,
|
||||
'subline' => $this->slideSubline,
|
||||
'price' => $this->slidePrice,
|
||||
'original_price' => $this->slideOriginalPrice,
|
||||
'tag_text' => $this->slideTagText,
|
||||
'bullets' => $this->slideBullets,
|
||||
'disclaimer' => $this->slideDisclaimer,
|
||||
'qr_url' => $this->slideQrUrl,
|
||||
'qr_title' => $this->slideQrTitle,
|
||||
'contact' => $this->slideContact,
|
||||
'show_brand_text' => $this->slideShowBrandText,
|
||||
'brand_tagline' => $this->slideBrandTagline,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function getActiveFlag(): bool
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => $this->videoIsActive,
|
||||
'footer' => $this->footerIsActive,
|
||||
'media' => $this->mediaIsActive,
|
||||
'slide' => $this->slideIsActive,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
private function defaultItemType(): string
|
||||
{
|
||||
return match ($this->version->type) {
|
||||
DisplayVersionType::VideoDisplay => 'video',
|
||||
DisplayVersionType::B2in => 'media',
|
||||
DisplayVersionType::Offers => 'slide',
|
||||
};
|
||||
}
|
||||
|
||||
private function resetItemForm(): void
|
||||
{
|
||||
$this->itemId = null;
|
||||
$this->itemType = '';
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
$this->mediaType = 'image';
|
||||
$this->mediaCategory = 'immobilien';
|
||||
$this->mediaUrl = '';
|
||||
$this->mediaHeadline = '';
|
||||
$this->mediaSubline = '';
|
||||
$this->mediaDuration = 10;
|
||||
$this->mediaIsActive = true;
|
||||
$this->slideType = 'product-hero';
|
||||
$this->slideDuration = 8000;
|
||||
$this->slideImageUrl = '';
|
||||
$this->slideBadge = '';
|
||||
$this->slideEyebrow = '';
|
||||
$this->slideTitle = '';
|
||||
$this->slideSubline = '';
|
||||
$this->slidePrice = '';
|
||||
$this->slideOriginalPrice = '';
|
||||
$this->slideTagText = '';
|
||||
$this->slideBullets = [];
|
||||
$this->slideDisclaimer = '';
|
||||
$this->slideQrUrl = '';
|
||||
$this->slideQrTitle = '';
|
||||
$this->slideContact = '';
|
||||
$this->slideShowBrandText = false;
|
||||
$this->slideBrandTagline = '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$items = $this->version->items()->get()->groupBy('item_type');
|
||||
|
||||
return view('livewire.admin.cms.display-version-editor', [
|
||||
'items' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
dev/file-upload/cms/DisplayVersionList.php
Normal file
102
dev/file-upload/cms/DisplayVersionList.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionList extends Component
|
||||
{
|
||||
public $showCreateModal = false;
|
||||
|
||||
public $newName = '';
|
||||
|
||||
public $newType = '';
|
||||
|
||||
public function openCreateModal(): void
|
||||
{
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
$this->showCreateModal = true;
|
||||
}
|
||||
|
||||
public function createVersion(): void
|
||||
{
|
||||
$this->validate([
|
||||
'newName' => 'required|string|max:255',
|
||||
'newType' => 'required|string|in:video-display,b2in,offers',
|
||||
], [
|
||||
'newName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
'newType.required' => 'Bitte wählen Sie einen Typ aus.',
|
||||
]);
|
||||
|
||||
$version = DisplayVersion::create([
|
||||
'name' => $this->newName,
|
||||
'type' => $this->newType,
|
||||
'settings' => $this->defaultSettingsForType($this->newType),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->showCreateModal = false;
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
|
||||
session()->flash('success', 'Version "'.$version->name.'" wurde erstellt!');
|
||||
|
||||
$this->redirect(
|
||||
route('admin.cms.display-version-edit', $version),
|
||||
navigate: true
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteVersion(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$name = $version->name;
|
||||
$version->delete();
|
||||
|
||||
session()->flash('success', 'Version "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$version->update(['is_active' => ! $version->is_active]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultSettingsForType(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'b2in' => [
|
||||
'theme' => 'dark',
|
||||
'footer_name' => '',
|
||||
'footer_url' => '',
|
||||
'transition' => ['type' => 'crossfade', 'duration_ms' => 800],
|
||||
'default_image_duration' => 10,
|
||||
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
|
||||
'display_active' => true,
|
||||
],
|
||||
'offers' => [
|
||||
'loop' => true,
|
||||
'transition' => ['type' => 'fade', 'duration' => 600],
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$versions = DisplayVersion::withCount(['items', 'displays'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-version-list', [
|
||||
'versions' => $versions,
|
||||
'types' => DisplayVersionType::cases(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
dev/file-upload/cms/MediaLibraryUploader.php
Normal file
45
dev/file-upload/cms/MediaLibraryUploader.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MediaLibraryUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $uploads = [];
|
||||
|
||||
public function updatedUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'uploads' => 'nullable|array|max:20',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
|
||||
foreach ($this->uploads as $file) {
|
||||
$media = $service->storeUpload($file);
|
||||
$this->dispatch('media-library-uploaded', mediaId: $media->id);
|
||||
}
|
||||
|
||||
$this->uploads = [];
|
||||
}
|
||||
|
||||
public function removeUpload(int $index): void
|
||||
{
|
||||
if (isset($this->uploads[$index])) {
|
||||
unset($this->uploads[$index]);
|
||||
$this->uploads = array_values($this->uploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.cms.media-library-uploader');
|
||||
}
|
||||
}
|
||||
139
dev/file-upload/cms/MediaPicker.php
Normal file
139
dev/file-upload/cms/MediaPicker.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Models\CmsMedia;
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class MediaPicker extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
use WithPagination;
|
||||
|
||||
public ?int $value = null;
|
||||
|
||||
public string $field = 'media_id';
|
||||
|
||||
public string $type = 'image';
|
||||
|
||||
public string $profile = 'thumbnail';
|
||||
|
||||
public string $label = 'Bild auswählen';
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $quickUploads = [];
|
||||
|
||||
public function mount(?int $value = null): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function openPicker(): void
|
||||
{
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function selectMedia(int $id): void
|
||||
{
|
||||
$media = CmsMedia::find($id);
|
||||
if (! $media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($media->isImage() && $this->profile) {
|
||||
$service = app(MediaConversionService::class);
|
||||
if (! $media->hasConversion($this->profile)) {
|
||||
$service->convert($media, $this->profile);
|
||||
$media->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $media->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $media->id, url: $media->getConversionUrl($this->profile));
|
||||
}
|
||||
|
||||
public function clearSelection(): void
|
||||
{
|
||||
$this->value = null;
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: null, url: null);
|
||||
}
|
||||
|
||||
public function updatedQuickUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'quickUploads' => 'nullable|array|max:5',
|
||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
$lastMedia = null;
|
||||
|
||||
foreach ($this->quickUploads as $file) {
|
||||
$lastMedia = $service->storeUpload($file);
|
||||
|
||||
if ($lastMedia->isImage() && $this->profile) {
|
||||
$service->convert($lastMedia, $this->profile);
|
||||
$lastMedia->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->quickUploads = [];
|
||||
|
||||
if ($lastMedia) {
|
||||
$this->value = $lastMedia->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $lastMedia->id, url: $lastMedia->getConversionUrl($this->profile));
|
||||
}
|
||||
}
|
||||
|
||||
public function removeQuickUpload(int $index): void
|
||||
{
|
||||
if (isset($this->quickUploads[$index])) {
|
||||
unset($this->quickUploads[$index]);
|
||||
$this->quickUploads = array_values($this->quickUploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.admin.cms.media-picker', [
|
||||
'selectedMedia' => $this->resolveSelectedMedia(),
|
||||
'mediaItems' => $this->resolveMediaItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSelectedMedia(): ?CmsMedia
|
||||
{
|
||||
if (! $this->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CmsMedia::find($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, CmsMedia>
|
||||
*/
|
||||
private function resolveMediaItems(): LengthAwarePaginator
|
||||
{
|
||||
return CmsMedia::query()
|
||||
->when($this->type === 'image', fn ($q) => $q->images())
|
||||
->when($this->type === 'pdf', fn ($q) => $q->pdfs())
|
||||
->when($this->type === 'document', fn ($q) => $q->documents())
|
||||
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(18);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue