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

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\DisplayMedia;
use App\Services\DisplayMediaService;
use Illuminate\Console\Command;
class GenerateVideoThumbnails extends Command
{
protected $signature = 'display-media:generate-video-thumbnails
{--force : Regenerate posters even if a thumbnail already exists}';
protected $description = 'Generate poster frames for uploaded display media videos using ffmpeg';
public function handle(DisplayMediaService $service): int
{
$query = DisplayMedia::query()
->where('type', 'video')
->where('source_type', 'upload');
if (! $this->option('force')) {
$query->whereNull('thumbnail_path');
}
$videos = $query->get();
if ($videos->isEmpty()) {
$this->info('Keine Videos zum Verarbeiten gefunden.');
return self::SUCCESS;
}
$this->info(sprintf('%d Video(s) werden verarbeitet...', $videos->count()));
$generated = 0;
$failed = 0;
foreach ($videos as $video) {
$thumbnailPath = $service->generateVideoThumbnail($video);
if ($thumbnailPath !== null) {
$video->update(['thumbnail_path' => $thumbnailPath]);
$this->line(" <info>✓</info> {$video->getDisplayName()}");
$generated++;
} else {
$this->warn("{$video->getDisplayName()} (Poster konnte nicht erzeugt werden)");
$failed++;
}
}
$this->newLine();
$this->info(sprintf('Fertig: %d erzeugt, %d fehlgeschlagen.', $generated, $failed));
return self::SUCCESS;
}
}

View file

@ -16,7 +16,9 @@ class DisplayPreviewController extends Controller
->where('preview_token', $token)
->firstOrFail();
return response()->file(public_path('_cabinet/display/index.html'));
return response()->file(public_path('_cabinet/display/index.html'), [
'Cache-Control' => 'no-cache, must-revalidate',
]);
}
public function config(string $token, DisplayPlaylistConfigBuilder $configBuilder): JsonResponse

View file

@ -13,14 +13,21 @@ class ModulePreviewController extends Controller
{
public function show(DisplayVersion $module): BinaryFileResponse
{
return response()->file(public_path('_cabinet/display/index.html'));
return $this->playerResponse();
}
public function showItem(DisplayVersion $module, DisplayVersionItem $item): BinaryFileResponse
{
abort_unless($item->display_version_id === $module->id, 404);
return response()->file(public_path('_cabinet/display/index.html'));
return $this->playerResponse();
}
private function playerResponse(): BinaryFileResponse
{
return response()->file(public_path('_cabinet/display/index.html'), [
'Cache-Control' => 'no-cache, must-revalidate',
]);
}
public function config(DisplayVersion $module, DisplayPlaylistConfigBuilder $configBuilder): JsonResponse

View file

@ -30,6 +30,9 @@ class DisplayList extends Component
public $addVersionSelect = null;
/** @var array<int> */
public $versionsToAdd = [];
public $editingPlaylistStatus = DisplayPlaylist::STATUS_PUBLISHED;
public ?string $draftPreviewToken = null;
@ -75,6 +78,20 @@ class DisplayList extends Component
$this->persistDraftPreviewIfNeeded();
}
public function addSelectedVersions(): void
{
foreach ($this->versionsToAdd as $versionId) {
$id = (int) $versionId;
if ($id && ! in_array($id, $this->selectedVersionIds, true)) {
$this->selectedVersionIds[] = $id;
}
}
$this->versionsToAdd = [];
$this->persistDraftPreviewIfNeeded();
}
private function firstAvailableVersionId(): ?int
{
return DisplayVersion::active()
@ -175,10 +192,31 @@ class DisplayList extends Component
}
$display->draftPlaylist->delete();
$display->clearPreviewToken();
session()->flash('success', 'Entwurf wurde verworfen.');
}
public function rotatePreviewToken(int $displayId): void
{
$display = Display::with('draftPlaylist')->findOrFail($displayId);
if (! $display->draftPlaylist) {
session()->flash('success', 'Für dieses Display gibt es keinen Entwurf.');
return;
}
$display->rotatePreviewToken();
if ($this->displayId === $display->id && $this->editingPlaylistStatus === DisplayPlaylist::STATUS_DRAFT) {
$this->draftPreviewToken = $display->preview_token;
$this->previewFrameRefreshCounter++;
}
session()->flash('success', 'Vorschau-Link wurde neu erzeugt. Der alte Link ist jetzt ungültig.');
}
public function publishDraft(int $displayId): void
{
$display = Display::with(['draftPlaylist.modules'])->findOrFail($displayId);
@ -201,6 +239,8 @@ class DisplayList extends Component
return $display->draftPlaylist->fresh('modules');
});
$display->clearPreviewToken();
session()->flash('success', 'Entwurf wurde veröffentlicht.');
}
@ -314,6 +354,7 @@ class DisplayList extends Component
$this->displayIsActive = true;
$this->displayIsTest = false;
$this->addVersionSelect = null;
$this->versionsToAdd = [];
$this->editingPlaylistStatus = DisplayPlaylist::STATUS_PUBLISHED;
$this->draftPreviewToken = null;
$this->previewFrameRefreshCounter = 0;

View file

@ -117,8 +117,7 @@ class DisplayMediaPicker extends Component
->active()
->when($this->type === 'image', fn ($q) => $q->images())
->when($this->type === 'video', fn ($q) => $q->videos())
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%")
->orWhere('title', 'like', "%{$this->search}%"))
->when($this->search, fn ($q) => $q->search($this->search))
->orderByDesc('created_at')
->paginate(18);
}

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;
}

View file

@ -54,7 +54,20 @@ class DisplayVersionList extends Component
public function deleteVersion(int $id): void
{
$version = DisplayVersion::findOrFail($id);
$version = DisplayVersion::query()
->withCount([
'playlistItems as displays_count' => fn ($query) => $query
->join('display_playlists', 'display_playlist_items.display_playlist_id', '=', 'display_playlists.id')
->select(DB::raw('count(distinct display_playlists.display_id)')),
])
->findOrFail($id);
if ($version->displays_count > 0) {
session()->flash('error', 'Modul "'.$version->name.'" wird noch von '.$version->displays_count.' Display(s) verwendet und kann nicht gelöscht werden. Entfernen Sie es zuerst aus den betroffenen Bespielungen.');
return;
}
$name = $version->name;
$version->delete();

View file

@ -45,10 +45,10 @@ class QuickStatus extends Component
],
];
public function mount(): void
public function mount(?string $k = null): void
{
$validKey = config('domains.cabinet_status_key');
$key = request()->get('key');
$key = $k ?? request()->query('k');
if (! $validKey || $key !== $validKey) {
$this->authorized = false;
@ -92,6 +92,13 @@ class QuickStatus extends Component
'noticeSubtext.max' => 'Subtext max. 80 Zeichen.',
]);
$showsNotice = in_array($this->storeStatus, ['notice', 'warning'], true);
if (! $showsNotice) {
$this->noticeHeadline = '';
$this->noticeSubtext = '';
}
CabinetTabletSetting::current()->update([
'store_status' => $this->storeStatus,
'notice_headline' => $this->noticeHeadline ?: null,

View file

@ -64,4 +64,20 @@ class Display extends Model
return $this->preview_token;
}
public function rotatePreviewToken(): string
{
$this->preview_token = Str::random(40);
$this->save();
return $this->preview_token;
}
public function clearPreviewToken(): void
{
if ($this->preview_token !== null) {
$this->preview_token = null;
$this->save();
}
}
}

View file

@ -150,4 +150,12 @@ class DisplayMedia extends Model
{
return $query->where('collection', $collection);
}
public function scopeSearch(Builder $query, string $term): Builder
{
return $query->where(function (Builder $query) use ($term): void {
$query->where('filename', 'like', "%{$term}%")
->orWhere('title', 'like', "%{$term}%");
});
}
}

View file

@ -7,6 +7,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
class DisplayMediaService
{
@ -34,7 +35,7 @@ class DisplayMediaService
}
}
return DisplayMedia::create([
$media = DisplayMedia::create([
'filename' => $filename,
'disk' => 'public',
'path' => $relativePath,
@ -45,6 +46,80 @@ class DisplayMediaService
'collection' => $collection,
'metadata' => ! empty($metadata) ? $metadata : null,
]);
if ($type === 'video') {
$thumbnailPath = $this->generateVideoThumbnail($media);
if ($thumbnailPath !== null) {
$media->update(['thumbnail_path' => $thumbnailPath]);
}
}
return $media;
}
/**
* Generate a poster frame for an uploaded video using ffmpeg.
*
* Returns the relative thumbnail path on the media's disk, or null when
* generation is not possible (e.g. ffmpeg missing or an unreadable file).
*/
public function generateVideoThumbnail(DisplayMedia $media): ?string
{
if (! $media->isVideo() || ! $media->isUpload() || ! $media->path) {
return null;
}
$disk = Storage::disk($media->disk);
$videoPath = $disk->path($media->path);
if (! is_file($videoPath)) {
return null;
}
$thumbnailRelativePath = preg_replace('/\.[^.\/]+$/', '', $media->path).'-poster.jpg';
$thumbnailPath = $disk->path($thumbnailRelativePath);
if (! is_dir(dirname($thumbnailPath))) {
@mkdir(dirname($thumbnailPath), 0755, true);
}
// Try a frame ~1s in first (avoids black intro frames); fall back to
// the very first frame for clips shorter than one second.
foreach (['1', '0'] as $seekSeconds) {
if ($this->extractFrame($videoPath, $thumbnailPath, $seekSeconds) && filesize($thumbnailPath) > 0) {
return $thumbnailRelativePath;
}
}
if (is_file($thumbnailPath)) {
@unlink($thumbnailPath);
}
return null;
}
private function extractFrame(string $videoPath, string $thumbnailPath, string $seekSeconds): bool
{
$process = new Process([
'ffmpeg',
'-y',
'-ss', $seekSeconds,
'-i', $videoPath,
'-frames:v', '1',
'-vf', 'scale=640:-2',
'-q:v', '3',
$thumbnailPath,
]);
$process->setTimeout(60);
try {
$process->run();
} catch (\Throwable) {
return false;
}
return $process->isSuccessful() && is_file($thumbnailPath);
}
/**

View file

@ -143,25 +143,40 @@ class DisplayPlaylistConfigBuilder
*/
private function offersData(DisplayVersion $module, Collection $items): array
{
$slides = $items->where('item_type', 'slide')->values()->map(fn ($item) => [
'type' => $item->content['type'] ?? 'product-hero',
'duration' => $item->content['duration'] ?? 8000,
'image_url' => $item->content['image_url'] ?? '',
'badge_text' => $item->content['badge_text'] ?? '',
'eyebrow' => $item->content['eyebrow'] ?? '',
'title' => $item->content['title'] ?? '',
'subline' => $item->content['subline'] ?? '',
'price' => $item->content['price'] ?? '',
'original_price' => $item->content['original_price'] ?? '',
'tag_text' => $item->content['tag_text'] ?? '',
'bullets' => $item->content['bullets'] ?? [],
'disclaimer' => $item->content['disclaimer'] ?? '',
'qr_url' => $item->content['qr_url'] ?? '',
'qr_title' => $item->content['qr_title'] ?? '',
'contact' => $item->content['contact'] ?? '',
'show_brand_text' => $item->content['show_brand_text'] ?? false,
'brand_tagline' => $item->content['brand_tagline'] ?? '',
]);
$slides = $items->where('item_type', 'slide')->values()->map(function ($item) {
$content = $item->content;
return [
'type' => $content['type'] ?? 'detail',
'show_logo' => $content['show_logo'] ?? true,
'logo_url' => $content['logo_url'] ?? '',
'brand_text' => $content['brand_text'] ?? '',
'duration' => $content['duration'] ?? 8000,
'image_url' => $content['image_url'] ?? '',
'badge_text' => $content['badge_text'] ?? '',
'show_badge' => $content['show_badge'] ?? ($content['badge_text'] ?? '') !== '',
'eyebrow' => $content['eyebrow'] ?? '',
'show_eyebrow' => $content['show_eyebrow'] ?? ($content['eyebrow'] ?? '') !== '',
'title' => $content['title'] ?? '',
'subline' => $content['subline'] ?? '',
'show_subline' => $content['show_subline'] ?? ($content['subline'] ?? '') !== '',
'price' => $content['price'] ?? '',
'original_price' => $content['original_price'] ?? '',
'strike_original_price' => $content['strike_original_price'] ?? false,
'tag_text' => $content['tag_text'] ?? '',
'show_price' => $content['show_price'] ?? ($content['price'] ?? '') !== '',
'bullets' => $content['bullets'] ?? [],
'show_bullets' => $content['show_bullets'] ?? ! empty($content['bullets']),
'disclaimer' => $content['disclaimer'] ?? '',
'show_disclaimer' => $content['show_disclaimer'] ?? ($content['disclaimer'] ?? '') !== '',
'qr_url' => $content['qr_url'] ?? '',
'qr_title' => $content['qr_title'] ?? '',
'show_qr' => $content['show_qr'] ?? ($content['qr_url'] ?? '') !== '',
'contact' => $content['contact'] ?? '',
'show_contact' => $content['show_contact'] ?? ($content['contact'] ?? '') !== '',
'brand_tagline' => $content['brand_tagline'] ?? '',
];
});
return [
'type' => 'offers',

View file

@ -21,6 +21,11 @@ class DisplayModuleSettings
'theme' => 'dark',
'header_logo_url' => '../assets/b2in-logo-positive.svg',
'header_claim' => 'Connecting Design & Property',
'logo_position' => 'top-left',
'claim_position' => 'top-right',
'show_logo' => true,
'show_claim' => true,
'show_footer' => true,
'footer_url' => 'B2in.eu',
'footer_name' => '',
'footer_prefix' => 'by',
@ -35,12 +40,6 @@ class DisplayModuleSettings
],
DisplayVersionType::Offers->value => [
'loop' => true,
'logo_url' => '../logo-cabinet-300.png',
'brand_text' => 'Bielefeld',
'footer_claim' => '',
'footer_url' => '',
'qr_default_title' => 'Kontakt',
'qr_subtitle' => 'QR scannen',
'transition' => [
'type' => 'fade',
'duration' => 600,