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>
This commit is contained in:
Kevin Adametz 2026-05-29 15:57:33 +00:00
parent 9262132325
commit 6c6d683b9a
42 changed files with 2267 additions and 13905 deletions

View file

@ -6,18 +6,18 @@
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Slides') }}</flux:heading>
<flux:subheading>{{ __('Angebots-Slides werden in der angegebenen Reihenfolge angezeigt') }}</flux:subheading>
<flux:heading size="lg">{{ __('Angebote') }}</flux:heading>
<flux:subheading>{{ __('Angebote werden im einheitlichen Detail-Layout in der angegebenen Reihenfolge angezeigt') }}</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'slide')" icon="plus">
{{ __('Slide hinzufügen') }}
{{ __('Angebot hinzufügen') }}
</flux:button>
</div>
@if($slides->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.presentation-chart-bar class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Slides vorhanden.') }}</p>
<p>{{ __('Noch keine Angebote vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@ -42,29 +42,30 @@
</div>
<div class="flex-1 min-w-0">
@php
$c = $item->content;
$enabledBlocks = collect([
'Badge' => ($c['show_badge'] ?? ! empty($c['badge_text'])) && ! empty($c['badge_text']),
'Aufzählung' => ($c['show_bullets'] ?? ! empty($c['bullets'])) && ! empty($c['bullets']),
'Preis' => ($c['show_price'] ?? ! empty($c['price'])) && ! empty($c['price']),
'QR' => ($c['show_qr'] ?? ! empty($c['qr_url'])) && ! empty($c['qr_url']),
'Kontakt' => ($c['show_contact'] ?? ! empty($c['contact'])) && ! empty($c['contact']),
])->filter()->keys();
@endphp
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<flux:badge color="amber" size="sm">
{{ match($item->content['type'] ?? '') {
'intro' => 'Intro',
'product-hero' => 'Produkt-Hero',
'product-details' => 'Produkt-Details',
'product-impulse' => 'Produkt-Impuls',
default => $item->content['type'] ?? '',
} }}
</flux:badge>
<span class="font-semibold text-sm">{{ $item->content['title'] ?? '' }}</span>
<span class="font-semibold text-sm truncate">{{ $c['title'] ?? '' }}</span>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
@if(!empty($item->content['price']))
<span class="font-medium">{{ $item->content['price'] }}</span>
@endif
<span>{{ number_format(($item->content['duration'] ?? 8000) / 1000, 1) }}s</span>
@if(!empty($item->content['badge_text']))
<span>{{ $item->content['badge_text'] }}</span>
<div class="flex flex-wrap items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<span>{{ number_format(($c['duration'] ?? 8000) / 1000, 1) }}s</span>
@if(!empty($c['price']) && ($c['show_price'] ?? ! empty($c['price'])))
<span class="font-medium">{{ $c['price'] }}</span>
@endif
@foreach($enabledBlocks as $block)
<flux:badge color="zinc" size="sm">{{ $block }}</flux:badge>
@endforeach
</div>
</div>

View file

@ -1,25 +1,74 @@
@if($version->type->value === 'b2in')
@php
$footerShown = ($settings['show_footer'] ?? true) !== false;
$showLogo = ($settings['show_logo'] ?? true) !== false;
$showClaim = ($settings['show_claim'] ?? true) !== false;
$logoPos = $settings['logo_position'] ?? 'top-left';
$claimPos = $settings['claim_position'] ?? 'top-right';
$brandPositions = [
'top-left' => __('Oben links'),
'top-right' => __('Oben rechts'),
'bottom-left' => __('Unten links'),
'bottom-right' => __('Unten rechts'),
];
@endphp
<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>
<flux:heading size="sm">{{ __('Marke') }}</flux:heading>
<flux:subheading>{{ __('Logo und Claim. Standardmäßig oben im Header. Die Ecken lassen sich frei wählen.') }}</flux:subheading>
<livewire:admin.cms.display-media-picker
:value="null"
field="settings.header_logo_url"
type="image"
label="Header-Logo aus Mediathek"
label="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" />
<div class="flex items-end gap-3">
<x-media-thumb :url="$settings['header_logo_url'] ?? ''" />
<flux:input wire:model.live.debounce.500ms="settings.header_logo_url" label="Logo URL" placeholder="../assets/b2in-logo-positive.svg" class="flex-1" />
</div>
<flux:input wire:model="settings.header_claim" label="Claim" placeholder="Connecting Design & Property" />
<div class="flex flex-wrap gap-6">
<flux:switch wire:model.live="settings.show_logo" label="Logo anzeigen" />
<flux:switch wire:model.live="settings.show_claim" label="Claim anzeigen" />
</div>
@if($showLogo)
<flux:select wire:model.live="settings.logo_position" label="Logo-Position">
@foreach($brandPositions as $value => $label)
<option value="{{ $value }}" @disabled($footerShown && str_starts_with($value, 'bottom'))>{{ $label }}</option>
@endforeach
</flux:select>
@endif
@if($showClaim)
<flux:select wire:model.live="settings.claim_position" label="Claim-Position">
@foreach($brandPositions as $value => $label)
<option value="{{ $value }}"
@disabled(($showLogo && $value === $logoPos) || ($footerShown && str_starts_with($value, 'bottom')))>
{{ $label }}{{ ($showLogo && $value === $logoPos) ? ' '.__('(Logo)') : '' }}
</option>
@endforeach
</flux:select>
@endif
@if($footerShown)
<flux:callout variant="secondary" icon="information-circle">
<flux:callout.text>{{ __('Untere Ecken sind nur verfügbar, wenn der Footer ausgeblendet ist.') }}</flux:callout.text>
</flux:callout>
@endif
</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:switch wire:model.live="settings.show_footer" label="Footer anzeigen"
description="Blendet die Fußzeile mit Domain, Name und QR-Code ein oder aus." />
<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"
<flux:input wire:model="settings.footer_url" label="Footer Domain" placeholder="z.B. b2in.eu" />
<flux:input wire:model="settings.qr_url" label="QR-URL (optional)" placeholder="https://b2in.e"
description="Leer = QR-Code nutzt die Footer-Domain." />
</div>
<flux:select wire:model="settings.transition.type" label="Transition">
@ -32,30 +81,14 @@
<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)" />
<flux:callout variant="secondary" icon="information-circle">
<flux:callout.text>{{ __('Logo, Marken-Text, QR-Code und Kontakt werden je Angebot direkt am Element gepflegt.') }}</flux:callout.text>
</flux:callout>
@elseif($version->type->value === 'video-display')
<flux:input wire:model="settings.qr_label" label="QR-Label im Footer" placeholder="Website" />
@endif

View file

@ -16,40 +16,43 @@
</flux:button>
</div>
@if($videos->isEmpty())
@if ($videos->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.film class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Videos vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($videos as $index => $item)
@foreach ($videos as $index => $item)
<div wire:key="item-{{ $item->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 transition">
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 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@if ($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost"
icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
@if($index < count($videos) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@if ($index < count($videos) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs"
variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600">
</flux:button>
@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>
<x-media-thumb :url="$item->content['filename'] ?? ''" size="h-16 w-12" />
<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">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="font-semibold text-sm">{{ $item->content['title'] ?? $item->content['filename'] ?? '' }}</span>
<span
class="font-semibold text-sm">{{ $item->content['title'] ?? ($item->content['filename'] ?? '') }}</span>
</div>
@php
$videoSource = $item->content['filename'] ?? '';
$isMediaLibrarySource = str_starts_with($videoSource, '/storage/') || str_starts_with($videoSource, 'http');
$isMediaLibrarySource =
str_starts_with($videoSource, '/storage/') || str_starts_with($videoSource, 'http');
@endphp
<div class="flex flex-wrap items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<flux:badge size="sm" :color="$isMediaLibrarySource ? 'sky' : 'zinc'">
@ -61,9 +64,13 @@
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost"
:icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost"
icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})"
wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost"
icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach
@ -76,34 +83,40 @@
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Footer-Inhalte') }}</flux:heading>
<flux:subheading>{{ __('Inhalte werden im Footer rotiert') }}</flux:subheading>
<flux:subheading>{{ __('Inhalte werden im Footer rotiert / ohne Inhalte bleibt der untere Teil frei.') }}
</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'footer')" icon="plus">
{{ __('Inhalt hinzufügen') }}
</flux:button>
</div>
@if($footers->isEmpty())
@if ($footers->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.document-text class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Footer-Inhalte vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($footers as $index => $item)
@foreach ($footers as $index => $item)
<div wire:key="item-{{ $item->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 transition">
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 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@if ($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs"
variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600">
</flux:button>
@endif
@if($index < count($footers) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@if ($index < count($footers) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs"
variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600">
</flux:button>
@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="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>
@ -117,16 +130,20 @@
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400">
{{ $item->content['subline'] ?? '' }}
@if(!empty($item->content['url']))
@if (!empty($item->content['url']))
<span class="ml-2">{{ Str::limit($item->content['url'], 40) }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost"
:icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost"
icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})"
wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost"
icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach