b2in/resources/views/livewire/admin/cms/display-list.blade.php
Kevin Adametz 6c6d683b9a Display CMS Optimierungen 29-05-2026
- Mediathek: Video-Vorschaubilder statt Icons (FFmpeg-Thumbnails + Backfill-Command), Kategorie "Sonstiges"
- B2in Media-Picker zeigt alle Medientypen, Typ wird automatisch erkannt; Thumbnail-Preview vor allen Medien-URL-Feldern
- B2in Marke/Footer: Footer ein/aus, Logo+Claim frei positionierbar (Ecken) mit Constraints, separate Anzeige-Schalter
- Angebote-Modul dynamisch: kein Slide-Typ mehr, einheitliches Detail-Layout mit ein-/ausblendbaren Bloecken, Logo/Brand pro Slide, Streichpreis-Option
- Player: leere Module stoppen Endlosschleife, dynamische Layout-Anpassung bei verstecktem Footer/Header
- Fix: Script-Ladereihenfolge (Livewire vor Flux), entfernte stale public/flux/flux.js, Modal-Crash beim Aktualisieren behoben

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:57:33 +00:00

427 lines
26 KiB
PHP

<div>
<flux:header class="mb-6">
<flux:heading size="xl">{{ __('Displays') }}</flux:heading>
<flux:subheading>{{ __('Verwalten Sie Live-Bespielungen und Entwürfe je physischem Display') }}</flux:subheading>
</flux:header>
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
@php
$displayPlayerUrl = rtrim(config('display.player_url') ?: 'https://cabinet.b2in.eu/display', '/');
$displayOverviewUrl = $displayPlayerUrl.'/';
@endphp
<flux:card class="mb-6 border-blue-200 bg-blue-50/70 dark:border-blue-500/30 dark:bg-blue-950/20">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<flux:heading size="lg">{{ __('Öffentliche Display-Übersicht') }}</flux:heading>
<flux:text class="mt-1">
{{ __('Hier sehen Sie alle aktiven Live-Displays und können die Wiedergabe direkt öffnen.') }}
</flux:text>
<div class="mt-2 text-xs font-mono text-blue-700 dark:text-blue-300">
{{ $displayOverviewUrl }}
</div>
</div>
<flux:button href="{{ $displayOverviewUrl }}" target="_blank" variant="primary" icon="arrow-top-right-on-square">
{{ __('Display-Übersicht öffnen') }}
</flux:button>
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Physische Displays') }}</flux:heading>
<flux:subheading>{{ __('Live bleibt stabil, Entwürfe können vorbereitet und gezielt veröffentlicht werden') }}</flux:subheading>
</div>
<flux:button wire:click="openModal" icon="plus" variant="primary">
{{ __('Display hinzufügen') }}
</flux:button>
</div>
@if($displays->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.tv class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Displays vorhanden. Fügen Sie Ihr erstes Display hinzu!') }}</p>
</div>
@else
<div class="space-y-4">
@foreach($displays as $display)
@php
$liveDisplayUrl = $displayPlayerUrl.'/?id='.$display->id;
$liveApiUrl = url('/api/display/'.$display->id.'/config');
@endphp
<div wire:key="display-{{ $display->id }}"
class="rounded-xl border p-4 transition {{ $display->is_test ? 'border-amber-300 bg-amber-50/70 dark:border-amber-500/50 dark:bg-amber-950/20' : 'border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800' }}">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<flux:badge :color="$display->is_active ? 'green' : 'zinc'" size="sm">
{{ $display->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
@if($display->is_test)
<flux:badge color="amber" size="sm">{{ __('Test-Display') }}</flux:badge>
@endif
<span class="font-semibold text-sm text-zinc-900 dark:text-zinc-100">{{ $display->name }}</span>
@if($display->location)
<span class="text-xs text-zinc-500 dark:text-zinc-400">{{ $display->location }}</span>
@endif
</div>
<div class="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
{{ __('Display-ID') }}: {{ $display->id }}
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleActive({{ $display->id }})"
size="sm"
variant="ghost"
:icon="$display->is_active ? 'eye-slash' : 'eye'">
</flux:button>
<flux:button wire:click="deleteDisplay({{ $display->id }})"
wire:confirm="Möchten Sie dieses Display wirklich löschen?"
size="sm"
variant="ghost"
icon="trash"
class="text-red-600 hover:text-red-700">
</flux:button>
</div>
</div>
<div class="mt-4 grid gap-4 lg:grid-cols-2">
<div class="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900/60">
<div class="mb-3 flex items-center justify-between gap-3">
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-green-600 dark:text-green-400">{{ __('Live') }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ optional($display->livePlaylist?->updated_at)->format('d.m.Y H:i') ?? __('Noch nicht veröffentlicht') }}
</div>
</div>
<flux:badge color="green" size="sm">{{ __('Veröffentlicht') }}</flux:badge>
</div>
@if($display->livePlaylist?->modules->isNotEmpty())
<div class="flex flex-wrap items-center gap-1.5">
@foreach($display->livePlaylist->modules as $idx => $module)
@if($idx > 0)
<flux:icon.chevron-right class="w-3 h-3 text-zinc-400" />
@endif
<flux:badge color="{{ match($module->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $module->name }}
</flux:badge>
@endforeach
</div>
@else
<div class="rounded border border-dashed border-amber-300 p-3 text-xs text-amber-700 dark:border-amber-500/60 dark:text-amber-300">
{{ __('Keine Live-Bespielung vorhanden') }}
</div>
@endif
<div class="mt-4 flex flex-wrap items-center gap-3">
<flux:button wire:click="openModal({{ $display->id }}, 'published')"
size="xs"
variant="ghost"
icon="pencil">
{{ __('Live bearbeiten') }}
</flux:button>
<a href="{{ $liveDisplayUrl }}"
target="_blank"
class="inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400">
<flux:icon.play class="w-3 h-3" />
{{ __('Vorschau') }}
</a>
</div>
<div class="mt-3">
<label class="mb-1 block text-xs font-medium text-zinc-500 dark:text-zinc-400">
{{ __('Live-URL zum Kopieren') }}
</label>
<div class="flex gap-2">
<input type="text"
readonly
value="{{ $liveDisplayUrl }}"
onclick="this.select()"
class="min-w-0 flex-1 rounded-md border border-zinc-200 bg-zinc-50 px-2 py-1.5 text-xs text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">
<button type="button"
onclick="navigator.clipboard?.writeText(@js($liveDisplayUrl))"
class="rounded-md border border-zinc-200 px-2 py-1.5 text-xs text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800">
{{ __('Kopieren') }}
</button>
</div>
</div>
<div class="mt-3 flex justify-end">
<a href="{{ $liveApiUrl }}"
target="_blank"
class="inline-flex items-center gap-1 text-xs text-zinc-400 hover:text-zinc-600 hover:underline dark:hover:text-zinc-300">
<flux:icon.code-bracket class="w-3 h-3" />
{{ __('API') }}
</a>
</div>
</div>
<div class="rounded-lg border border-dashed border-zinc-300 bg-white p-4 dark:border-zinc-600 dark:bg-zinc-900/60">
<div class="mb-3 flex items-center justify-between gap-3">
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400">{{ __('Entwurf') }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ optional($display->draftPlaylist?->updated_at)->format('d.m.Y H:i') ?? __('Kein Entwurf') }}
</div>
</div>
<flux:badge :color="$display->draftPlaylist ? 'amber' : 'zinc'" size="sm">
{{ $display->draftPlaylist ? __('In Arbeit') : __('Leer') }}
</flux:badge>
</div>
@if($display->draftPlaylist)
@if($display->draftPlaylist->modules->isNotEmpty())
<div class="flex flex-wrap items-center gap-1.5">
@foreach($display->draftPlaylist->modules as $idx => $module)
@if($idx > 0)
<flux:icon.chevron-right class="w-3 h-3 text-zinc-400" />
@endif
<flux:badge color="{{ match($module->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $module->name }}
</flux:badge>
@endforeach
</div>
@else
<div class="rounded border border-dashed border-zinc-300 p-3 text-xs text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
{{ __('Entwurf ist leer') }}
</div>
@endif
<div class="mt-4 flex flex-wrap items-center gap-3">
<flux:button wire:click="openModal({{ $display->id }}, 'draft')"
size="xs"
variant="ghost"
icon="pencil">
{{ __('Entwurf bearbeiten') }}
</flux:button>
<a href="/preview/{{ $display->preview_token }}"
target="_blank"
class="inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400">
<flux:icon.play class="w-3 h-3" />
{{ __('Test-URL') }}
</a>
<flux:button wire:click="rotatePreviewToken({{ $display->id }})"
wire:confirm="Vorschau-Link neu erzeugen? Der bisherige Link wird damit ungültig."
size="xs"
variant="ghost"
icon="arrow-path">
{{ __('Link erneuern') }}
</flux:button>
<flux:button wire:click="publishDraft({{ $display->id }})"
wire:confirm="Diesen Entwurf veröffentlichen und den Live-Stand ersetzen?"
size="xs"
variant="primary">
{{ __('Veröffentlichen') }}
</flux:button>
<flux:button wire:click="discardDraft({{ $display->id }})"
wire:confirm="Diesen Entwurf wirklich verwerfen?"
size="xs"
variant="ghost">
{{ __('Verwerfen') }}
</flux:button>
</div>
@else
<div class="rounded border border-dashed border-zinc-300 p-3 text-xs text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
{{ __('Noch kein Entwurf. Beim Anlegen wird der Live-Stand kopiert.') }}
</div>
<div class="mt-4">
<flux:button wire:click="createDraft({{ $display->id }})" size="sm" variant="ghost" icon="document-plus">
{{ __('Entwurf anlegen') }}
</flux:button>
</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>
{{-- Display Modal --}}
<flux:modal :open="$showModal" wire:model="showModal">
<form wire:submit.prevent="save">
@php
$isDraftEditor = $displayId && $editingPlaylistStatus === \App\Models\DisplayPlaylist::STATUS_DRAFT;
$draftPreviewUrl = $draftPreviewToken ? url('/preview/'.$draftPreviewToken).'?refresh='.$previewFrameRefreshCounter : null;
@endphp
<div>
<div class="space-y-6">
<div>
<flux:heading size="lg">
@if(! $displayId)
{{ __('Display hinzufügen') }}
@elseif($isDraftEditor)
{{ __('Entwurf bearbeiten') }}
@else
{{ __('Live-Bespielung bearbeiten') }}
@endif
</flux:heading>
</div>
<flux:input wire:model="displayName" label="Name" placeholder="z.B. Display 1 - Eingang" />
@error('displayName') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:input wire:model="displayLocation" label="Standort (optional)" placeholder="z.B. Schaufenster links" />
{{-- Version Playlist --}}
<div>
<flux:heading size="sm" class="mb-2">
{{ $isDraftEditor ? __('Entwurfs-Bespielung') : __('Live-Bespielung') }}
</flux:heading>
<flux:subheading class="mb-3">{{ __('Module werden in dieser Reihenfolge als Schleife abgespielt') }}</flux:subheading>
@if(count($selectedVersionIds) > 0)
<div class="space-y-2 mb-3">
@foreach($selectedVersionIds as $index => $versionId)
@php $ver = $versions->firstWhere('id', $versionId); @endphp
@if($ver)
<div wire:key="playlist-{{ $index }}-{{ $versionId }}"
class="flex items-center gap-2 p-2 bg-zinc-100 dark:bg-zinc-700 rounded border border-zinc-200 dark:border-zinc-600">
<span class="text-xs text-zinc-400 font-mono w-5 text-center">{{ $index + 1 }}</span>
<flux:badge color="{{ match($ver->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $ver->type->label() }}
</flux:badge>
<span class="text-sm flex-1">{{ $ver->name }}</span>
<div class="flex items-center gap-1">
<flux:button wire:click="moveVersion({{ $index }}, 'up')"
type="button"
size="xs"
variant="ghost"
icon="chevron-up"
:disabled="$index === 0">
</flux:button>
<flux:button wire:click="moveVersion({{ $index }}, 'down')"
type="button"
size="xs"
variant="ghost"
icon="chevron-down"
:disabled="$index === count($selectedVersionIds) - 1">
</flux:button>
<flux:button wire:click="removeVersion({{ $index }})"
type="button"
size="xs"
variant="ghost"
icon="x-mark"
class="text-red-500">
</flux:button>
</div>
</div>
@endif
@endforeach
</div>
@else
<div class="text-center py-4 text-zinc-400 text-sm border border-dashed border-zinc-300 dark:border-zinc-600 rounded mb-3">
{{ __('Noch keine Module hinzugefügt') }}
</div>
@endif
@php
$availableVersions = $versions->reject(fn ($version) => in_array($version->id, $selectedVersionIds, true));
@endphp
@if($availableVersions->isNotEmpty())
<div class="flex gap-2">
<div class="flex-1">
<flux:select wire:model="versionsToAdd"
variant="listbox"
multiple
searchable
placeholder="Module hinzufügen..."
selected-suffix="Module ausgewählt">
@foreach($availableVersions as $version)
<flux:select.option value="{{ $version->id }}">{{ $version->name }} ({{ $version->type->label() }})</flux:select.option>
@endforeach
</flux:select>
</div>
<flux:button wire:click="addSelectedVersions"
type="button"
icon="plus"
size="sm"
variant="ghost">
{{ __('Hinzufügen') }}
</flux:button>
</div>
@else
<div class="rounded border border-dashed border-zinc-300 p-3 text-xs text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
{{ __('Alle verfügbaren Module sind bereits hinzugefügt.') }}
</div>
@endif
</div>
<flux:checkbox wire:model="displayIsActive" label="Display aktiv" />
<flux:checkbox wire:model="displayIsTest" label="Als Test-Display hervorheben" />
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="closeModal" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ $displayId ? __('Aktualisieren') : __('Hinzufügen') }}
</flux:button>
</div>
@if($isDraftEditor)
<div class="space-y-3 border-t border-zinc-200 pt-6 dark:border-zinc-700">
<div>
<flux:heading size="sm">{{ __('Live-Vorschau') }}</flux:heading>
<flux:subheading>{{ __('Aktualisiert sich nach Modul-Änderungen automatisch') }}</flux:subheading>
</div>
<div class="mx-auto aspect-[9/16] w-full max-w-[320px] overflow-hidden rounded-xl border border-zinc-200 bg-black shadow-sm dark:border-zinc-700">
@if($draftPreviewUrl)
<iframe
wire:key="draft-preview-{{ $previewFrameRefreshCounter }}"
src="{{ $draftPreviewUrl }}"
class="h-full w-full border-0"
loading="lazy"
title="{{ __('Entwurfs-Vorschau') }}"
></iframe>
@else
<div class="flex h-full items-center justify-center p-6 text-center text-xs text-zinc-400">
{{ __('Für diesen Entwurf ist noch keine Vorschau-URL verfügbar.') }}
</div>
@endif
</div>
@if($draftPreviewUrl)
<a href="{{ $draftPreviewUrl }}"
target="_blank"
class="mx-auto flex max-w-[320px] items-center justify-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400">
<flux:icon.arrow-top-right-on-square class="w-3 h-3" />
{{ __('Vollbild öffnen') }}
</a>
@endif
</div>
@endif
</div>
</div>
</form>
</flux:modal>
</div>