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:
parent
9262132325
commit
6c6d683b9a
42 changed files with 2267 additions and 13905 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue