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

@ -3,10 +3,10 @@
namespace App\Livewire\Admin\Cms;
use App\Enums\DisplayVersionType;
use App\Models\DisplayMedia;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use App\Support\DisplayModuleSettings;
use Illuminate\Support\Facades\File;
use Livewire\Attributes\On;
use Livewire\Component;
@ -56,8 +56,12 @@ class DisplayVersionEditor extends Component
public bool $mediaIsActive = true;
// Offers: Slide fields
public string $slideType = 'product-hero';
// Offers: Slide fields (single dynamic detail layout)
public bool $slideShowLogo = true;
public string $slideLogoUrl = '';
public string $slideBrandText = '';
public int $slideDuration = 8000;
@ -65,30 +69,46 @@ class DisplayVersionEditor extends Component
public string $slideBadge = '';
public bool $slideShowBadge = true;
public string $slideEyebrow = '';
public bool $slideShowEyebrow = true;
public string $slideTitle = '';
public string $slideSubline = '';
public bool $slideShowSubline = false;
public string $slidePrice = '';
public string $slideOriginalPrice = '';
public bool $slideStrikeOriginalPrice = false;
public string $slideTagText = '';
public bool $slideShowPrice = false;
/** @var array<string> */
public array $slideBullets = [];
public bool $slideShowBullets = true;
public string $slideDisclaimer = '';
public bool $slideShowDisclaimer = false;
public string $slideQrUrl = '';
public string $slideQrTitle = '';
public bool $slideShowQr = true;
public string $slideContact = '';
public bool $slideShowBrandText = false;
public bool $slideShowContact = true;
public string $slideBrandTagline = '';
@ -99,32 +119,25 @@ class DisplayVersionEditor extends Component
public array $settings = [];
/** @var array<string> */
public array $availableVideos = [];
public int $previewFrameRefreshCounter = 0;
/** @var array<string> */
public const BRAND_POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
public function mount(DisplayVersion $displayVersion): void
{
$this->version = $displayVersion;
$this->versionName = $displayVersion->name;
$this->settings = $this->settingsWithDefaults();
if ($this->version->type === DisplayVersionType::VideoDisplay) {
$this->loadAvailableVideos();
}
$this->normalizeBrandPositions();
}
public function loadAvailableVideos(): void
public function updated(string $name): 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();
if (str_starts_with($name, 'settings.show_footer')
|| str_starts_with($name, 'settings.logo_position')
|| str_starts_with($name, 'settings.claim_position')) {
$this->normalizeBrandPositions();
}
}
@ -160,12 +173,56 @@ class DisplayVersionEditor extends Component
public function saveSettings(): void
{
$this->normalizeBrandPositions();
$this->version->update(['settings' => $this->settings]);
$this->showSettingsModal = false;
$this->refreshModulePreview();
session()->flash('success', 'Einstellungen gespeichert!');
}
/**
* Keep the B2in logo/claim corners consistent:
* - bottom corners are only valid while the footer is hidden,
* - the claim can never sit in the same corner as the logo.
*/
private function normalizeBrandPositions(): void
{
if ($this->version->type !== DisplayVersionType::B2in) {
return;
}
$footerShown = ($this->settings['show_footer'] ?? true) !== false;
$allowed = $footerShown ? ['top-left', 'top-right'] : self::BRAND_POSITIONS;
$logo = $this->settings['logo_position'] ?? 'top-left';
$claim = $this->settings['claim_position'] ?? 'top-right';
$logo = $this->moveIntoAllowed($logo, $allowed);
$claim = $this->moveIntoAllowed($claim, $allowed);
if ($claim === $logo) {
$claim = collect($allowed)->first(fn (string $position) => $position !== $logo) ?? $claim;
}
$this->settings['logo_position'] = $logo;
$this->settings['claim_position'] = $claim;
}
/**
* @param array<string> $allowed
*/
private function moveIntoAllowed(string $position, array $allowed): string
{
if (in_array($position, $allowed, true)) {
return $position;
}
// Pull bottom corners up to the matching top corner when forbidden.
$fallback = str_replace('bottom-', 'top-', $position);
return in_array($fallback, $allowed, true) ? $fallback : ($allowed[0] ?? 'top-left');
}
// ========================================
// ITEM CRUD
// ========================================
@ -276,10 +333,20 @@ class DisplayVersionEditor extends Component
'videoFilename' => $this->videoFilename = $url,
'mediaUrl' => $this->mediaUrl = $url,
'slideImageUrl' => $this->slideImageUrl = $url,
'slideLogoUrl' => $this->slideLogoUrl = $url,
'settings.header_logo_url' => $this->settings['header_logo_url'] = $url,
'settings.logo_url' => $this->settings['logo_url'] = $url,
default => null,
};
// The media type for a B2in playlist item is derived from the chosen
// media the type selector in the form is only an informational hint.
if ($field === 'mediaUrl' && $mediaId) {
$media = DisplayMedia::find($mediaId);
if ($media) {
$this->mediaType = $media->isVideo() ? 'video' : 'image';
}
}
}
public function addBullet(): void
@ -306,33 +373,43 @@ class DisplayVersionEditor extends Component
private function loadItemContent(DisplayVersionItem $item): void
{
$content = $item->content;
$isActive = (bool) $item->is_active;
match ($item->item_type) {
'video' => $this->loadVideoContent($content),
'footer' => $this->loadFooterContent($content),
'media' => $this->loadMediaContent($content),
'slide' => $this->loadSlideContent($content),
'video' => $this->loadVideoContent($content, $isActive),
'footer' => $this->loadFooterContent($content, $isActive),
'media' => $this->loadMediaContent($content, $isActive),
'slide' => $this->loadSlideContent($content, $isActive),
default => null,
};
}
private function loadVideoContent(array $content): void
/**
* @param array<string, mixed> $content
*/
private function loadVideoContent(array $content, bool $isActive): void
{
$this->videoFilename = $content['filename'] ?? '';
$this->videoTitle = $content['title'] ?? '';
$this->videoPosition = $content['position'] ?? 25;
$this->videoIsActive = true;
$this->videoIsActive = $isActive;
}
private function loadFooterContent(array $content): void
/**
* @param array<string, mixed> $content
*/
private function loadFooterContent(array $content, bool $isActive): void
{
$this->footerHeadline = $content['headline'] ?? '';
$this->footerSubline = $content['subline'] ?? '';
$this->footerUrl = $content['url'] ?? '';
$this->footerIsActive = true;
$this->footerIsActive = $isActive;
}
private function loadMediaContent(array $content): void
/**
* @param array<string, mixed> $content
*/
private function loadMediaContent(array $content, bool $isActive): void
{
$this->mediaType = $content['media_type'] ?? 'image';
$this->mediaCategory = $content['category'] ?? 'immobilien';
@ -340,12 +417,17 @@ class DisplayVersionEditor extends Component
$this->mediaHeadline = $content['headline'] ?? '';
$this->mediaSubline = $content['subline'] ?? '';
$this->mediaDuration = $content['duration_seconds'] ?? 10;
$this->mediaIsActive = true;
$this->mediaIsActive = $isActive;
}
private function loadSlideContent(array $content): void
/**
* @param array<string, mixed> $content
*/
private function loadSlideContent(array $content, bool $isActive): void
{
$this->slideType = $content['type'] ?? 'product-hero';
$this->slideShowLogo = $content['show_logo'] ?? true;
$this->slideLogoUrl = $content['logo_url'] ?? '';
$this->slideBrandText = $content['brand_text'] ?? '';
$this->slideDuration = $content['duration'] ?? 8000;
$this->slideImageUrl = $content['image_url'] ?? '';
$this->slideBadge = $content['badge_text'] ?? '';
@ -354,15 +436,26 @@ class DisplayVersionEditor extends Component
$this->slideSubline = $content['subline'] ?? '';
$this->slidePrice = $content['price'] ?? '';
$this->slideOriginalPrice = $content['original_price'] ?? '';
$this->slideStrikeOriginalPrice = $content['strike_original_price'] ?? false;
$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;
$this->slideIsActive = $isActive;
// Show flags fall back to "is there content?" so slides created before
// the dynamic detail layout keep rendering exactly as they did.
$this->slideShowBadge = $content['show_badge'] ?? ($content['badge_text'] ?? '') !== '';
$this->slideShowEyebrow = $content['show_eyebrow'] ?? ($content['eyebrow'] ?? '') !== '';
$this->slideShowSubline = $content['show_subline'] ?? ($content['subline'] ?? '') !== '';
$this->slideShowBullets = $content['show_bullets'] ?? ! empty($content['bullets']);
$this->slideShowPrice = $content['show_price'] ?? ($content['price'] ?? '') !== '';
$this->slideShowDisclaimer = $content['show_disclaimer'] ?? ($content['disclaimer'] ?? '') !== '';
$this->slideShowQr = $content['show_qr'] ?? ($content['qr_url'] ?? '') !== '';
$this->slideShowContact = $content['show_contact'] ?? ($content['contact'] ?? '') !== '';
}
/**
@ -390,22 +483,33 @@ class DisplayVersionEditor extends Component
'duration_seconds' => $this->mediaDuration,
],
'slide' => [
'type' => $this->slideType,
'type' => 'detail',
'show_logo' => $this->slideShowLogo,
'logo_url' => $this->slideLogoUrl,
'brand_text' => $this->slideBrandText,
'duration' => $this->slideDuration,
'image_url' => $this->slideImageUrl,
'badge_text' => $this->slideBadge,
'show_badge' => $this->slideShowBadge,
'eyebrow' => $this->slideEyebrow,
'show_eyebrow' => $this->slideShowEyebrow,
'title' => $this->slideTitle,
'subline' => $this->slideSubline,
'show_subline' => $this->slideShowSubline,
'price' => $this->slidePrice,
'original_price' => $this->slideOriginalPrice,
'strike_original_price' => $this->slideStrikeOriginalPrice,
'tag_text' => $this->slideTagText,
'bullets' => $this->slideBullets,
'show_price' => $this->slideShowPrice,
'bullets' => array_values(array_filter($this->slideBullets, fn (string $bullet) => trim($bullet) !== '')),
'show_bullets' => $this->slideShowBullets,
'disclaimer' => $this->slideDisclaimer,
'show_disclaimer' => $this->slideShowDisclaimer,
'qr_url' => $this->slideQrUrl,
'qr_title' => $this->slideQrTitle,
'show_qr' => $this->slideShowQr,
'contact' => $this->slideContact,
'show_brand_text' => $this->slideShowBrandText,
'show_contact' => $this->slideShowContact,
'brand_tagline' => $this->slideBrandTagline,
],
default => [],
@ -451,22 +555,32 @@ class DisplayVersionEditor extends Component
$this->mediaSubline = '';
$this->mediaDuration = 10;
$this->mediaIsActive = true;
$this->slideType = 'product-hero';
$this->slideShowLogo = true;
$this->slideLogoUrl = '';
$this->slideBrandText = '';
$this->slideDuration = 8000;
$this->slideImageUrl = '';
$this->slideBadge = '';
$this->slideShowBadge = true;
$this->slideEyebrow = '';
$this->slideShowEyebrow = true;
$this->slideTitle = '';
$this->slideSubline = '';
$this->slideShowSubline = false;
$this->slidePrice = '';
$this->slideOriginalPrice = '';
$this->slideStrikeOriginalPrice = false;
$this->slideTagText = '';
$this->slideShowPrice = false;
$this->slideBullets = [];
$this->slideShowBullets = true;
$this->slideDisclaimer = '';
$this->slideShowDisclaimer = false;
$this->slideQrUrl = '';
$this->slideQrTitle = '';
$this->slideShowQr = true;
$this->slideContact = '';
$this->slideShowBrandText = false;
$this->slideShowContact = true;
$this->slideBrandTagline = '';
$this->slideIsActive = true;
}