12-05-2026 admin, Panel Displays
This commit is contained in:
parent
0762e3beac
commit
6a65354f4c
43 changed files with 3273 additions and 410 deletions
|
|
@ -183,7 +183,7 @@
|
|||
|
||||
<flux:navlist.group :heading="__('Cabinet')" class="grid mb-4">
|
||||
<flux:navlist.group expandable
|
||||
:expanded="request()->routeIs(['admin.cms.display-dashboard', 'admin.cms.display-media', 'admin.cms.display-versions', 'admin.cms.display-version-edit', 'admin.cms.displays', 'admin.cms.cabinet', 'admin.cms.cabinet-tablet'])"
|
||||
:expanded="request()->routeIs(['admin.cms.display-dashboard', 'admin.cms.display-media', 'admin.cms.display-modules', 'admin.cms.display-module-edit', 'admin.cms.display-versions', 'admin.cms.display-version-edit', 'admin.cms.displays', 'admin.cms.cabinet', 'admin.cms.cabinet-tablet'])"
|
||||
heading="Store Displays" class="grid">
|
||||
<flux:navlist.item icon="squares-2x2" :href="route('admin.cms.display-dashboard')"
|
||||
:current="request()->routeIs('admin.cms.display-dashboard')" wire:navigate>{{ __('Übersicht') }}
|
||||
|
|
@ -191,8 +191,8 @@
|
|||
<flux:navlist.item icon="photo" :href="route('admin.cms.display-media')"
|
||||
:current="request()->routeIs('admin.cms.display-media')" wire:navigate>{{ __('Mediathek') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="rectangle-group" :href="route('admin.cms.display-versions')"
|
||||
:current="request()->routeIs(['admin.cms.display-versions', 'admin.cms.display-version-edit'])" wire:navigate>{{ __('Versionen') }}
|
||||
<flux:navlist.item icon="rectangle-group" :href="route('admin.cms.display-modules')"
|
||||
:current="request()->routeIs(['admin.cms.display-modules', 'admin.cms.display-module-edit', 'admin.cms.display-versions', 'admin.cms.display-version-edit'])" wire:navigate>{{ __('Module') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="tv" :href="route('admin.cms.displays')"
|
||||
:current="request()->routeIs('admin.cms.displays')" wire:navigate>{{ __('Displays') }}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ title('Store Displays');
|
|||
$stats = computed(fn () => [
|
||||
'displays' => Display::count(),
|
||||
'displays_active' => Display::where('is_active', true)->count(),
|
||||
'versions' => DisplayVersion::count(),
|
||||
'versions_active' => DisplayVersion::active()->count(),
|
||||
'modules' => DisplayVersion::count(),
|
||||
'modules_active' => DisplayVersion::active()->count(),
|
||||
'items' => DisplayVersionItem::count(),
|
||||
'items_active' => DisplayVersionItem::where('is_active', true)->count(),
|
||||
'type_video' => DisplayVersion::ofType(DisplayVersionType::VideoDisplay)->count(),
|
||||
|
|
@ -41,7 +41,7 @@ $tabletStatus = computed(function () {
|
|||
<div>
|
||||
<div class="mb-6">
|
||||
<flux:heading size="xl">Store Displays</flux:heading>
|
||||
<flux:text class="mt-1">Displays, Inhalts-Versionen und Info-Tablet im Cabinet Showroom verwalten.</flux:text>
|
||||
<flux:text class="mt-1">Displays, Inhalts-Module und Info-Tablet im Cabinet Showroom verwalten.</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
|
|
@ -57,13 +57,13 @@ $tabletStatus = computed(function () {
|
|||
</flux:card>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.cms.display-versions') }}" wire:navigate>
|
||||
<a href="{{ route('admin.cms.display-modules') }}" wire:navigate>
|
||||
<flux:card class="hover:border-purple-500 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:icon name="rectangle-group" class="text-purple-500" />
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $this->stats['versions'] }}</flux:heading>
|
||||
<flux:text class="text-sm">Versionen ({{ $this->stats['versions_active'] }} aktiv)</flux:text>
|
||||
<flux:heading size="lg">{{ $this->stats['modules'] }}</flux:heading>
|
||||
<flux:text class="text-sm">Module ({{ $this->stats['modules_active'] }} aktiv)</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -125,14 +125,14 @@ $tabletStatus = computed(function () {
|
|||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Versions-Typen Übersicht --}}
|
||||
{{-- Modul-Typen Übersicht --}}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3 mt-4">
|
||||
<flux:card>
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:icon name="film" class="text-purple-400" />
|
||||
<div>
|
||||
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">Video-Display</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_video'] }} {{ $this->stats['type_video'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_video'] }} {{ $this->stats['type_video'] === 1 ? 'Modul' : 'Module' }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -141,7 +141,7 @@ $tabletStatus = computed(function () {
|
|||
<flux:icon name="photo" class="text-blue-400" />
|
||||
<div>
|
||||
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">B2in Display</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_b2in'] }} {{ $this->stats['type_b2in'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_b2in'] }} {{ $this->stats['type_b2in'] === 1 ? 'Modul' : 'Module' }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -150,7 +150,7 @@ $tabletStatus = computed(function () {
|
|||
<flux:icon name="tag" class="text-amber-400" />
|
||||
<div>
|
||||
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">Angebote</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_offers'] }} {{ $this->stats['type_offers'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
|
||||
<flux:text class="text-xs">{{ $this->stats['type_offers'] }} {{ $this->stats['type_offers'] === 1 ? 'Modul' : 'Module' }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -172,8 +172,8 @@ $tabletStatus = computed(function () {
|
|||
</p>
|
||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Mediathek</strong> - Zentrale Verwaltung aller Bilder und Videos fuer die Displays. Dateien bis 200 MB direkt hochladen oder groessere Videos als externe URL (Google Drive, OneDrive) einbinden.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Versionen</strong> – Content-Pakete, die auf den Displays abgespielt werden. Jede Version hat einen bestimmten Typ und enthält passende Inhalte (Videos, Bilder oder Angebots-Slides).</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Displays</strong> – Die physischen Bildschirme im Showroom. Jedem Display werden eine oder mehrere Versionen als Playlist zugewiesen.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Module</strong> – Wiederverwendbare Content-Pakete, die auf den Displays abgespielt werden. Jede Modul hat einen bestimmten Typ und enthält passende Inhalte (Videos, Bilder oder Angebots-Slides).</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Displays</strong> – Die physischen Bildschirme im Showroom. Jedem Display werden eine oder mehrere Module als Playlist zugewiesen.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Info-Tablet</strong> – Das Tablet an der Eingangstür des Showrooms. Hier verwalten Sie Öffnungszeiten, den aktuellen Store-Status und Hinweise für Besucher.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -191,9 +191,9 @@ $tabletStatus = computed(function () {
|
|||
</p>
|
||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Direkt-Upload:</strong> Bilder und Videos bis 200 MB direkt per Drag-and-drop oder Dateiauswahl hochladen. Die Dateien werden auf dem Server gespeichert und stehen sofort zur Verfügung.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Externe URLs:</strong> Für Videos über 200 MB (z. B. 4K-Showroom-Rundgänge) können Sie einen Freigabe-Link von Google Drive, OneDrive oder anderen Cloud-Diensten hinterlegen. Diese URL wird wie ein normales Medium in der Mediathek verwaltet und kann genauso in Versionen eingebunden werden.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Externe URLs:</strong> Für Videos über 200 MB (z. B. 4K-Showroom-Rundgänge) können Sie einen Freigabe-Link von Google Drive, OneDrive oder anderen Cloud-Diensten hinterlegen. Diese URL wird wie ein normales Medium in der Mediathek verwaltet und kann genauso in Module eingebunden werden.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Sammlungen:</strong> Ordnen Sie Medien in Sammlungen wie <em>immobilien</em>, <em>moebel</em> oder <em>brand</em>, um bei vielen Dateien den Überblick zu behalten.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienauswahl im Editor:</strong> Beim Bearbeiten einer Version erscheint ein „Aus Mediathek"-Button. Darüber öffnen Sie die Medienauswahl und können bestehende Medien wählen oder direkt neue hochladen.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienauswahl im Editor:</strong> Beim Bearbeiten eines Moduls erscheint ein „Aus Mediathek"-Button. Darüber öffnen Sie die Medienauswahl und können bestehende Medien wählen oder direkt neue hochladen.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -202,10 +202,10 @@ $tabletStatus = computed(function () {
|
|||
<div>
|
||||
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
|
||||
<flux:icon name="rectangle-group" class="size-4 text-purple-500" />
|
||||
Versionen & Versions-Typen
|
||||
Module & Modul-Typen
|
||||
</flux:heading>
|
||||
<p>
|
||||
Eine <strong class="font-medium text-zinc-800 dark:text-zinc-200">Version</strong> ist ein Content-Paket mit einem bestimmten Typ.
|
||||
Ein <strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul</strong> ist ein Content-Paket mit einem bestimmten Typ.
|
||||
Der Typ bestimmt, welche Art von Inhalten hinzugefügt werden können:
|
||||
</p>
|
||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||
|
|
@ -223,7 +223,7 @@ $tabletStatus = computed(function () {
|
|||
</li>
|
||||
</ul>
|
||||
<p class="mt-2">
|
||||
Innerhalb einer Version können Sie beliebig viele Inhalte anlegen, die Reihenfolge per Hoch/Runter-Sortierung festlegen und einzelne Einträge aktivieren oder deaktivieren.
|
||||
Innerhalb eines Moduls können Sie beliebig viele Inhalte anlegen, die Reihenfolge per Hoch/Runter-Sortierung festlegen und einzelne Einträge aktivieren oder deaktivieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ $tabletStatus = computed(function () {
|
|||
Ein <strong class="font-medium text-zinc-800 dark:text-zinc-200">Display</strong> repräsentiert einen physischen Bildschirm im Showroom.
|
||||
</p>
|
||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Versions-Zuweisung:</strong> Jedem Display können Sie eine oder mehrere Versionen zuordnen. Die Versionen werden in der festgelegten Reihenfolge als Playlist abgespielt.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul-Zuweisung:</strong> Jedem Display können Sie eine oder mehrere Module zuordnen. Die Module werden in der festgelegten Reihenfolge als Playlist abgespielt.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Aktiv/Inaktiv:</strong> Über den Aktiv-Status können Sie einzelne Displays vorübergehend deaktivieren, ohne die Konfiguration zu verlieren.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">API-Anbindung:</strong> Jedes Display ruft seine Inhalte über eine JSON-API ab (<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded">/api/display/{id}/config</code>). Änderungen werden beim nächsten Abruf automatisch übernommen.</li>
|
||||
</ul>
|
||||
|
|
@ -282,9 +282,9 @@ $tabletStatus = computed(function () {
|
|||
Typischer Workflow
|
||||
</flux:heading>
|
||||
<ol class="mt-2 ml-5 list-decimal space-y-1">
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Version erstellen</strong> – Unter „Versionen" eine neue Version mit passendem Typ anlegen (z. B. „Frühling 2026 Video").</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte hinzufügen</strong> – In der Version Videos, Medien oder Slides anlegen, Reihenfolge festlegen und aktivieren.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Display zuweisen</strong> – Unter „Displays" die Version einem physischen Bildschirm zuordnen.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul erstellen</strong> – Unter „Module" ein neues Modul mit passendem Typ anlegen (z. B. „Frühling 2026 Video").</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte hinzufügen</strong> – Im Modul Videos, Medien oder Slides anlegen, Reihenfolge festlegen und aktivieren.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Display zuweisen</strong> – Unter „Displays" das Modul einem physischen Bildschirm zuordnen.</li>
|
||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Fertig</strong> – Das Display lädt die neuen Inhalte automatisch über die API.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<flux:header class="mb-6">
|
||||
<flux:heading size="xl">{{ __('Displays') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie Ihre physischen Displays und weisen Sie ihnen Versionen zu') }}</flux:subheading>
|
||||
<flux:subheading>{{ __('Verwalten Sie Live-Bespielungen und Entwürfe je physischem Display') }}</flux:subheading>
|
||||
</flux:header>
|
||||
|
||||
@if (session()->has('success'))
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Physische Displays') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Jedem Display können mehrere Versionen als Playlist zugewiesen werden') }}</flux:subheading>
|
||||
<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') }}
|
||||
|
|
@ -27,80 +27,201 @@
|
|||
<p>{{ __('Noch keine Displays vorhanden. Fügen Sie Ihr erstes Display hinzu!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-4">
|
||||
@foreach($displays as $display)
|
||||
@php
|
||||
$liveDisplayUrl = url('/_cabinet/display/index.html').'?id='.$display->id;
|
||||
$liveApiUrl = url('/api/display/'.$display->id.'/config');
|
||||
@endphp
|
||||
<div wire:key="display-{{ $display->id }}"
|
||||
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
|
||||
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-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$display->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $display->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<span class="font-semibold text-sm">{{ $display->name }}</span>
|
||||
@if($display->location)
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">{{ $display->location }}</span>
|
||||
@endif
|
||||
<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>
|
||||
|
||||
@if($display->versions->isNotEmpty())
|
||||
<div class="flex flex-wrap items-center gap-1.5 mt-2">
|
||||
@foreach($display->versions as $idx => $version)
|
||||
@if($idx > 0)
|
||||
<flux:icon.chevron-right class="w-3 h-3 text-zinc-400" />
|
||||
@endif
|
||||
<flux:badge color="{{ match($version->type->value) {
|
||||
'video-display' => 'purple',
|
||||
'b2in' => 'blue',
|
||||
'offers' => 'amber',
|
||||
} }}" size="sm">
|
||||
{{ $version->name }}
|
||||
</flux:badge>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-xs text-amber-600 dark:text-amber-400 mt-1">
|
||||
{{ __('Keine Versionen zugewiesen') }}
|
||||
</div>
|
||||
@endif
|
||||
<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>
|
||||
|
||||
{{-- Direct display links --}}
|
||||
<div class="mt-2 flex items-center gap-4">
|
||||
<a href="/_cabinet/display/index.html?id={{ $display->id }}"
|
||||
target="_blank"
|
||||
class="text-xs text-blue-600 dark:text-blue-400 hover:underline inline-flex items-center gap-1">
|
||||
<flux:icon.play class="w-3 h-3" />
|
||||
Display öffnen
|
||||
</a>
|
||||
<a href="/api/display/{{ $display->id }}/config"
|
||||
target="_blank"
|
||||
class="text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:underline inline-flex items-center gap-1">
|
||||
<flux:icon.code-bracket class="w-3 h-3" />
|
||||
API
|
||||
</a>
|
||||
<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="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>
|
||||
<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>
|
||||
|
||||
<flux:button wire:click="openModal({{ $display->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="pencil">
|
||||
</flux:button>
|
||||
@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
|
||||
|
||||
<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 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="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
|
||||
|
|
@ -111,92 +232,157 @@
|
|||
{{-- Display Modal --}}
|
||||
<flux:modal :open="$showModal" wire:model="showModal">
|
||||
<form wire:submit.prevent="save">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $displayId ? __('Display bearbeiten') : __('Display hinzufügen') }}</flux:heading>
|
||||
</div>
|
||||
@php
|
||||
$isDraftEditor = $displayId && $editingPlaylistStatus === \App\Models\DisplayPlaylist::STATUS_DRAFT;
|
||||
$draftPreviewUrl = $draftPreviewToken ? url('/preview/'.$draftPreviewToken).'?refresh='.$previewFrameRefreshCounter : null;
|
||||
@endphp
|
||||
|
||||
<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
|
||||
<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="displayLocation" label="Standort (optional)" placeholder="z.B. Schaufenster links" />
|
||||
<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
|
||||
|
||||
{{-- Version Playlist --}}
|
||||
<div>
|
||||
<flux:heading size="sm" class="mb-2">{{ __('Versions-Playlist') }}</flux:heading>
|
||||
<flux:subheading class="mb-3">{{ __('Versionen werden in dieser Reihenfolge als Schleife abgespielt') }}</flux:subheading>
|
||||
<flux:input wire:model="displayLocation" label="Standort (optional)" placeholder="z.B. Schaufenster links" />
|
||||
|
||||
@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')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-up"
|
||||
:disabled="$index === 0">
|
||||
</flux:button>
|
||||
<flux:button wire:click="moveVersion({{ $index }}, 'down')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
:disabled="$index === count($selectedVersionIds) - 1">
|
||||
</flux:button>
|
||||
<flux:button wire:click="removeVersion({{ $index }})"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="x-mark"
|
||||
class="text-red-500">
|
||||
</flux:button>
|
||||
{{-- 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>
|
||||
</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 Versionen hinzugefügt') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<flux:select wire:model="addVersionSelect" placeholder="Version hinzufügen...">
|
||||
@foreach($versions as $version)
|
||||
<option value="{{ $version->id }}">{{ $version->name }} ({{ $version->type->label() }})</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
<flux:button wire:click="addVersion"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
variant="ghost">
|
||||
</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="addVersionSelect" placeholder="Modul hinzufügen...">
|
||||
@foreach($availableVersions as $version)
|
||||
<option value="{{ $version->id }}">{{ $version->name }} ({{ $version->type->label() }})</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
<flux:button wire:click="addVersion"
|
||||
type="button"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
variant="ghost">
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<flux:checkbox wire:model="displayIsActive" label="Display aktiv" />
|
||||
@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="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 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"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ $stats = computed(fn () => [
|
|||
$handleUploads = function () {
|
||||
$this->validate([
|
||||
'uploads' => 'nullable|array|max:10',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,mp4,webm,mov|max:204800',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,mp4,webm,mov|max:204800',
|
||||
]);
|
||||
|
||||
$service = app(DisplayMediaService::class);
|
||||
|
|
@ -205,10 +205,10 @@ $closeDetail = function () {
|
|||
<div class="flex items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<flux:file-upload wire:model="uploads" multiple
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,.jpg,.jpeg,.png,.webp,.mp4,.webm,.mov">
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm,.jpg,.jpeg,.png,.gif,.webp,.svg,.mp4,.webm,.mov">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Dateien hochladen"
|
||||
text="Bilder & Videos bis 200 MB - Drag & Drop oder klicken"
|
||||
text="Bilder inkl. SVG & Videos bis 200 MB - Drag & Drop oder klicken"
|
||||
with-progress />
|
||||
</flux:file-upload>
|
||||
@if (isset($uploads) && count($uploads) > 0)
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@
|
|||
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen (Dateiname oder Titel)..." icon="magnifying-glass" size="sm" />
|
||||
|
||||
<flux:file-upload wire:model="quickUploads" multiple
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,.jpg,.jpeg,.png,.webp,.mp4,.webm,.mov">
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm,.jpg,.jpeg,.png,.gif,.webp,.svg,.mp4,.webm,.mov">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Datei hochladen"
|
||||
text="Bilder und Videos bis 200 MB"
|
||||
text="Bilder inkl. SVG und Videos bis 200 MB"
|
||||
with-progress />
|
||||
</flux:file-upload>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:button :href="route('admin.cms.display-versions')" wire:navigate variant="ghost" icon="arrow-left" size="sm">
|
||||
<flux:button :href="route('admin.cms.display-modules')" wire:navigate variant="ghost" icon="arrow-left" size="sm">
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
<div>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
{{ $version->type->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<flux:subheading>{{ __('Version bearbeiten') }}</flux:subheading>
|
||||
<flux:subheading>{{ __('Modul bearbeiten') }}</flux:subheading>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -44,12 +44,37 @@
|
|||
<flux:card class="mb-6">
|
||||
<form wire:submit.prevent="saveName" class="flex items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<flux:input wire:model="versionName" label="Versionsname" />
|
||||
<flux:input wire:model="versionName" label="Modulname" />
|
||||
</div>
|
||||
<flux:button type="submit" variant="primary" size="sm">{{ __('Speichern') }}</flux:button>
|
||||
</form>
|
||||
</flux:card>
|
||||
|
||||
{{-- Modul-Vorschau --}}
|
||||
<flux:card class="mb-6">
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Modul-Vorschau') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Live gerenderte Einzelmodul-Vorschau im Display-Player') }}</flux:subheading>
|
||||
</div>
|
||||
<a href="{{ $this->modulePreviewUrl() }}"
|
||||
target="_blank"
|
||||
class="inline-flex items-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-Vorschau') }}
|
||||
</a>
|
||||
</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">
|
||||
<iframe
|
||||
wire:key="module-preview-{{ $previewFrameRefreshCounter }}"
|
||||
src="{{ $this->modulePreviewUrl() }}"
|
||||
class="h-full w-full border-0"
|
||||
title="{{ __('Modul-Vorschau') }}"
|
||||
></iframe>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Type-specific content sections --}}
|
||||
@if($version->type->value === 'video-display')
|
||||
@include('livewire.admin.cms.partials.version-editor-video', ['items' => $items])
|
||||
|
|
@ -59,8 +84,28 @@
|
|||
@include('livewire.admin.cms.partials.version-editor-offers', ['items' => $items])
|
||||
@endif
|
||||
|
||||
{{-- Module-level metadata --}}
|
||||
<flux:card class="mt-8 mb-6">
|
||||
<form wire:submit.prevent="saveSettings">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Meta-Einstellungen für dieses Modul') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Diese Werte gelten für die gesamte Media-Playlist bzw. alle Slides dieses Moduls.') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@include('livewire.admin.cms.partials.version-editor-settings', ['context' => 'inline'])
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" size="sm">
|
||||
{{ __('Meta-Einstellungen speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</flux:card>
|
||||
|
||||
{{-- Item Modal --}}
|
||||
<flux:modal :open="$showItemModal" wire:model="showItemModal">
|
||||
<flux:modal :open="$showItemModal" wire:model="showItemModal" class="w-full max-w-5xl">
|
||||
<form wire:submit.prevent="saveItem">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
|
|
@ -194,12 +239,38 @@
|
|||
<flux:checkbox wire:model="slideIsActive" label="Aktiv" />
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<div class="space-y-3 border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
<div>
|
||||
<flux:heading size="sm">{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Zeigt nur den aktuell bearbeiteten Inhalt im Display-Player') }}</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">
|
||||
<iframe
|
||||
wire:key="item-modal-module-preview-{{ $previewFrameRefreshCounter }}"
|
||||
src="{{ $this->itemPreviewUrl() }}"
|
||||
class="h-full w-full border-0"
|
||||
title="{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<a href="{{ $this->itemPreviewUrl() }}"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-3 border-t border-zinc-200 pt-4 dark:border-zinc-700">
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $itemId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" wire:click="closeItemModal" variant="ghost">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $itemId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
<flux:button type="button" wire:click="closeItemModal">
|
||||
{{ __('Schließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -215,31 +286,7 @@
|
|||
<flux:subheading>{{ $version->type->label() }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if($version->type->value === 'b2in')
|
||||
<flux:select wire:model="settings.theme" label="Theme">
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="settings.footer_name" label="Footer Name" placeholder="z.B. Marcel Scheibe" />
|
||||
<flux:input wire:model="settings.footer_url" label="Footer URL" placeholder="z.B. b2in.de" />
|
||||
<flux:select wire:model="settings.transition.type" label="Transition">
|
||||
<option value="crossfade">Crossfade</option>
|
||||
<option value="fade">Fade</option>
|
||||
<option value="slide">Slide</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="settings.transition.duration_ms" type="number" label="Transition-Dauer (ms)" />
|
||||
<flux:input wire:model="settings.default_image_duration" type="number" label="Standard-Bilddauer (Sek.)" />
|
||||
<flux:checkbox wire:model="settings.display_active" label="Display aktiv" />
|
||||
@elseif($version->type->value === 'offers')
|
||||
<flux:checkbox wire:model="settings.loop" label="Endlosschleife" />
|
||||
<flux:select wire:model="settings.transition.type" label="Transition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="slide">Slide</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="settings.transition.duration" type="number" label="Transition-Dauer (ms)" />
|
||||
@elseif($version->type->value === 'video-display')
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Keine speziellen Einstellungen für diesen Typ.') }}</p>
|
||||
@endif
|
||||
@include('livewire.admin.cms.partials.version-editor-settings', ['context' => 'modal'])
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="$set('showSettingsModal', false)" variant="ghost">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<flux:header class="mb-6">
|
||||
<flux:heading size="xl">{{ __('Display-Versionen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Erstellen und verwalten Sie Inhalts-Versionen für Ihre Displays') }}</flux:subheading>
|
||||
<flux:heading size="xl">{{ __('Display-Module') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Erstellen und verwalten Sie wiederverwendbare Inhalts-Module für Ihre Displays') }}</flux:subheading>
|
||||
</flux:header>
|
||||
|
||||
@if (session()->has('success'))
|
||||
|
|
@ -13,18 +13,18 @@
|
|||
<flux:card>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Versionen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Jede Version enthält Inhalte eines bestimmten Typs') }}</flux:subheading>
|
||||
<flux:heading size="lg">{{ __('Module') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Jedes Modul enthält Inhalte eines bestimmten Typs') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button wire:click="openCreateModal" icon="plus" variant="primary">
|
||||
{{ __('Version erstellen') }}
|
||||
{{ __('Modul erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if($versions->isEmpty())
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.rectangle-group class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>{{ __('Noch keine Versionen vorhanden. Erstellen Sie Ihre erste Version!') }}</p>
|
||||
<p>{{ __('Noch keine Module vorhanden. Erstellen Sie Ihr erstes Modul!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<flux:badge :color="$version->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $version->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<a href="{{ route('admin.cms.display-version-edit', $version) }}"
|
||||
<a href="{{ route('admin.cms.display-module-edit', $version) }}"
|
||||
wire:navigate
|
||||
class="font-semibold text-sm hover:underline">
|
||||
{{ $version->name }}
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
:icon="$version->is_active ? 'eye-slash' : 'eye'">
|
||||
</flux:button>
|
||||
|
||||
<flux:button :href="route('admin.cms.display-version-edit', $version)"
|
||||
<flux:button :href="route('admin.cms.display-module-edit', $version)"
|
||||
wire:navigate
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</flux:button>
|
||||
|
||||
<flux:button wire:click="deleteVersion({{ $version->id }})"
|
||||
wire:confirm="Möchten Sie diese Version wirklich löschen? Alle zugehörigen Inhalte werden ebenfalls gelöscht."
|
||||
wire:confirm="Möchten Sie dieses Modul wirklich löschen? Alle zugehörigen Inhalte werden ebenfalls gelöscht."
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
<form wire:submit.prevent="createVersion">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Neue Version erstellen') }}</flux:heading>
|
||||
<flux:heading size="lg">{{ __('Neues Modul erstellen') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="newName" label="Name" placeholder="z.B. Herbst 2025 Video" />
|
||||
|
|
|
|||
|
|
@ -34,6 +34,16 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="h-16 w-12 shrink-0 overflow-hidden rounded-lg bg-zinc-200 dark:bg-zinc-700">
|
||||
@if(($item->content['media_type'] ?? 'image') === 'image' && !empty($item->content['media_url']))
|
||||
<img src="{{ $item->content['media_url'] }}" alt="" class="h-full w-full object-cover">
|
||||
@else
|
||||
<div class="flex h-full w-full items-center justify-center text-[10px] font-semibold uppercase text-zinc-500">
|
||||
{{ ($item->content['media_type'] ?? 'image') === 'video' ? 'Video' : 'Bild' }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex h-28 w-20 shrink-0 flex-col rounded-lg border border-zinc-200 bg-white p-1.5 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="mb-1.5 h-14 rounded bg-zinc-100 bg-cover bg-center dark:bg-zinc-800"
|
||||
@if(!empty($item->content['image_url'])) style="background-image: url('{{ $item->content['image_url'] }}')" @endif></div>
|
||||
<div class="line-clamp-2 text-[10px] font-semibold leading-tight text-zinc-700 dark:text-zinc-200">{{ $item->content['title'] ?? 'Slide' }}</div>
|
||||
<div class="mt-auto truncate text-[9px] text-zinc-500">{{ $item->content['price'] ?? ($item->content['badge_text'] ?? '') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
@if($version->type->value === 'b2in')
|
||||
<flux:select wire:model="settings.theme" label="Theme">
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</flux:select>
|
||||
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<flux:heading size="sm">{{ __('Header') }}</flux:heading>
|
||||
<livewire:admin.cms.display-media-picker
|
||||
:value="null"
|
||||
field="settings.header_logo_url"
|
||||
type="image"
|
||||
label="Header-Logo aus Mediathek"
|
||||
:key="'picker-b2in-header-logo-' . $context . '-' . $version->id" />
|
||||
<flux:input wire:model="settings.header_logo_url" label="Header-Logo URL" placeholder="../assets/b2in-logo-positive.svg" />
|
||||
<flux:input wire:model="settings.header_claim" label="Claim" placeholder="Connecting Design & Property" />
|
||||
</div>
|
||||
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<flux:heading size="sm">{{ __('Footer & QR') }}</flux:heading>
|
||||
<flux:input wire:model="settings.footer_prefix" label="Footer-Präfix" placeholder="by" />
|
||||
<flux:input wire:model="settings.footer_name" label="Footer Name" placeholder="z.B. Marcel Scheibe" />
|
||||
<flux:input wire:model="settings.footer_url" label="Footer Domain" placeholder="z.B. b2in.de" />
|
||||
<flux:input wire:model="settings.qr_url" label="QR-URL (optional)" placeholder="https://b2in.de"
|
||||
description="Leer = QR-Code nutzt die Footer-Domain." />
|
||||
</div>
|
||||
<flux:select wire:model="settings.transition.type" label="Transition">
|
||||
<option value="crossfade">Crossfade</option>
|
||||
<option value="fade">Fade</option>
|
||||
<option value="slide">Slide</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="settings.transition.duration_ms" type="number" label="Transition-Dauer (ms)" />
|
||||
<flux:input wire:model="settings.default_image_duration" type="number" label="Standard-Bilddauer (Sek.)" />
|
||||
<flux:checkbox wire:model="settings.display_active" label="Display aktiv" />
|
||||
@elseif($version->type->value === 'offers')
|
||||
<flux:checkbox wire:model="settings.loop" label="Endlosschleife" />
|
||||
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<flux:heading size="sm">{{ __('Branding') }}</flux:heading>
|
||||
<livewire:admin.cms.display-media-picker
|
||||
:value="null"
|
||||
field="settings.logo_url"
|
||||
type="image"
|
||||
label="Logo aus Mediathek"
|
||||
:key="'picker-offers-logo-' . $context . '-' . $version->id" />
|
||||
<flux:input wire:model="settings.logo_url" label="Logo URL" placeholder="../logo-cabinet-300.png" />
|
||||
<flux:input wire:model="settings.brand_text" label="Brand-Text" placeholder="Bielefeld" />
|
||||
</div>
|
||||
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<flux:heading size="sm">{{ __('Footer & QR für alle Slides') }}</flux:heading>
|
||||
<flux:input wire:model="settings.footer_claim" label="Footer-Claim" placeholder="z.B. Planung • Beratung • Lieferung & Montage" />
|
||||
<flux:input wire:model="settings.footer_url" label="Web/QR-URL" placeholder="https://cabinet-bielefeld.de"
|
||||
description="Wird als QR-Ziel genutzt, wenn der einzelne Slide keine eigene QR-URL hat." />
|
||||
<flux:input wire:model="settings.qr_default_title" label="Standard QR-Titel" placeholder="Kontakt" />
|
||||
<flux:input wire:model="settings.qr_subtitle" label="QR-Unterzeile" placeholder="QR scannen" />
|
||||
</div>
|
||||
<flux:select wire:model="settings.transition.type" label="Transition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="slide">Slide</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="settings.transition.duration" type="number" label="Transition-Dauer (ms)" />
|
||||
@elseif($version->type->value === 'video-display')
|
||||
<flux:input wire:model="settings.qr_label" label="QR-Label im Footer" placeholder="Website" />
|
||||
@endif
|
||||
|
|
@ -36,6 +36,10 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex h-16 w-12 shrink-0 items-center justify-center rounded-lg bg-black text-[10px] font-semibold uppercase text-white">
|
||||
Video
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
|
||||
|
|
@ -92,6 +96,11 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex h-16 w-12 shrink-0 flex-col justify-end rounded-lg bg-zinc-900 p-1 text-[8px] text-white">
|
||||
<div class="truncate text-zinc-400">{{ $item->content['headline'] ?? 'Footer' }}</div>
|
||||
<div class="truncate font-semibold">{{ $item->content['subline'] ?? '' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@
|
|||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
{{ data_get($showUi, 'next_step_text') }}
|
||||
</p>
|
||||
<a href="/contact" class="mt-5 inline-flex w-full justify-center btn-primary-accent">
|
||||
<a href="#projekt-anfrage" class="mt-5 inline-flex w-full justify-center btn-primary-accent">
|
||||
{{ data_get($showUi, 'request_cta') }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -227,13 +227,13 @@
|
|||
{{ data_get($showUi, 'interest_text') }}
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col justify-center gap-3 sm:flex-row">
|
||||
<a href="/contact"
|
||||
<a href="#projekt-anfrage"
|
||||
class="btn-primary-accent px-8 py-4 text-lg">{{ data_get($showUi, 'interest_cta_consult') }}</a>
|
||||
<a href="{{ $overviewUrl }}#projekte"
|
||||
class="btn-secondary-accent px-8 py-4 text-lg">{{ data_get($showUi, 'interest_cta_more') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 text-left">
|
||||
<div id="projekt-anfrage" class="mt-10 scroll-mt-24 text-left">
|
||||
<livewire:web.components.sections.immobilien-contact-form :projectSlug="$project['slug'] ?? ''" :projectTitle="$project['title'] ?? ''"
|
||||
:showInterest="false" :submitLabel="data_get($ui, 'modal.request_submit', 'Anfrage absenden')"
|
||||
wire:key="azizi-project-detail-contact-form-{{ $project['slug'] ?? 'project' }}" />
|
||||
|
|
|
|||
|
|
@ -295,6 +295,8 @@
|
|||
<nav x-show="!selectedProject" x-cloak x-transition.opacity x-data="{
|
||||
open: false,
|
||||
active: '',
|
||||
navTop: 12,
|
||||
panelTop: 64,
|
||||
sections: @js($sidebarSections),
|
||||
update() {
|
||||
const offset = window.innerHeight * 0.35
|
||||
|
|
@ -307,6 +309,18 @@
|
|||
}
|
||||
this.active = current
|
||||
},
|
||||
updatePosition() {
|
||||
const header = document.getElementById('header')
|
||||
if (!header) {
|
||||
this.navTop = 12
|
||||
this.panelTop = 64
|
||||
return
|
||||
}
|
||||
|
||||
const rect = header.getBoundingClientRect()
|
||||
this.navTop = Math.max(12, rect.top + 12)
|
||||
this.panelTop = Math.max(64, rect.bottom + 8)
|
||||
},
|
||||
scrollToSection(id) {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) {
|
||||
|
|
@ -319,12 +333,15 @@
|
|||
return this.sections.find(section => section.id === this.active)?.label || this.sections[0]?.label || ''
|
||||
},
|
||||
}" x-init="update();
|
||||
updatePosition();
|
||||
window.addEventListener('scroll', () => update(), { passive: true });
|
||||
window.addEventListener('resize', () => update(), { passive: true });"
|
||||
window.addEventListener('scroll', () => updatePosition(), { passive: true });
|
||||
window.addEventListener('resize', () => { update(); updatePosition(); }, { passive: true });"
|
||||
@keydown.escape.window="open = false" aria-label="{{ data_get($ui, 'sidebar.toggle_open') }}"
|
||||
class="fixed right-14 top-3 z-[60] lg:hidden">
|
||||
<div x-show="open" x-transition.origin.top.right
|
||||
class="fixed inset-x-3 top-16 rounded-2xl border border-border bg-background p-3 shadow-2xl shadow-zinc-950/25">
|
||||
class="fixed right-[3.25rem] z-[60] md:hidden" :style="`top: ${navTop}px`">
|
||||
<div x-show="open" x-cloak x-transition.origin.top.right @click.outside="open = false"
|
||||
class="fixed inset-x-3 rounded-2xl border border-border bg-background p-3 shadow-2xl shadow-zinc-950/25"
|
||||
:style="`top: ${panelTop}px`">
|
||||
<div class="mb-3 flex items-center justify-between gap-3 border-b border-border pb-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
{{ data_get($ui, 'sidebar.mobile_label', 'Abschnitt') }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue