*/ 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 $slideShowContact = true; public string $slideBrandTagline = ''; public bool $slideIsActive = true; // Settings Modal public bool $showSettingsModal = false; public array $settings = []; public int $previewFrameRefreshCounter = 0; /** @var array */ 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(); $this->normalizeBrandPositions(); } public function updated(string $name): void { if (str_starts_with($name, 'settings.show_footer') || str_starts_with($name, 'settings.logo_position') || str_starts_with($name, 'settings.claim_position')) { $this->normalizeBrandPositions(); } } public function toggleTheme(): void { $settings = $this->settingsWithDefaults(); $settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark'; $this->version->update(['settings' => $settings]); $this->settings = $settings; $this->refreshModulePreview(); } public function saveName(): void { $this->validate([ 'versionName' => 'required|string|max:255', ]); $this->version->update(['name' => $this->versionName]); $this->refreshModulePreview(); session()->flash('success', 'Name aktualisiert!'); } // ======================================== // SETTINGS // ======================================== public function openSettingsModal(): void { $this->settings = $this->settingsWithDefaults(); $this->showSettingsModal = true; } 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 $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 // ======================================== public function openItemModal(?int $id = null, string $type = ''): void { $this->resetItemForm(); if ($id) { $item = DisplayVersionItem::findOrFail($id); $this->itemId = $item->id; $this->itemType = $item->item_type; $this->loadItemContent($item); } else { $this->itemType = $type ?: $this->defaultItemType(); } $this->showItemModal = true; } public function saveItem(): void { $content = $this->buildItemContent(); $isActive = $this->getActiveFlag(); if ($this->itemId) { $item = DisplayVersionItem::findOrFail($this->itemId); $item->update([ 'content' => $content, 'is_active' => $isActive, ]); session()->flash('success', 'Inhalt aktualisiert!'); } else { $maxSort = DisplayVersionItem::where('display_version_id', $this->version->id) ->where('item_type', $this->itemType) ->max('sort_order') ?? -1; $item = DisplayVersionItem::create([ 'display_version_id' => $this->version->id, 'item_type' => $this->itemType, 'content' => $content, 'sort_order' => $maxSort + 1, 'is_active' => $isActive, ]); session()->flash('success', 'Inhalt hinzugefügt!'); } $this->itemId = $item->id; $this->itemType = $item->item_type; $this->loadItemContent($item->fresh()); $this->showItemModal = true; $this->refreshModulePreview(); } public function deleteItem(int $id): void { DisplayVersionItem::findOrFail($id)->delete(); $this->refreshModulePreview(); session()->flash('success', 'Inhalt gelöscht!'); } public function toggleItemStatus(int $id): void { $item = DisplayVersionItem::findOrFail($id); $item->update(['is_active' => ! $item->is_active]); $this->refreshModulePreview(); } public function moveItem(int $id, string $direction): void { $item = DisplayVersionItem::findOrFail($id); $currentOrder = $item->sort_order; $swapItem = DisplayVersionItem::where('display_version_id', $this->version->id) ->where('item_type', $item->item_type) ->where('sort_order', $direction === 'up' ? $currentOrder - 1 : $currentOrder + 1) ->first(); if ($swapItem) { $item->update(['sort_order' => $swapItem->sort_order]); $swapItem->update(['sort_order' => $currentOrder]); $this->refreshModulePreview(); } } public function modulePreviewUrl(): string { return url('/preview/module/'.$this->version->id).'?refresh='.$this->previewFrameRefreshCounter; } public function itemPreviewUrl(): string { if (! $this->itemId) { return $this->modulePreviewUrl(); } return url('/preview/module/'.$this->version->id.'/item/'.$this->itemId).'?refresh='.$this->previewFrameRefreshCounter; } #[On('display-media-selected')] public function onDisplayMediaSelected(string $field, ?int $mediaId, ?string $url): void { if (! $url) { return; } match ($field) { '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, 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 { $this->slideBullets[] = ''; } public function removeBullet(int $index): void { unset($this->slideBullets[$index]); $this->slideBullets = array_values($this->slideBullets); } public function closeItemModal(): void { $this->showItemModal = false; $this->resetItemForm(); } // ======================================== // HELPERS // ======================================== private function loadItemContent(DisplayVersionItem $item): void { $content = $item->content; $isActive = (bool) $item->is_active; match ($item->item_type) { 'video' => $this->loadVideoContent($content, $isActive), 'footer' => $this->loadFooterContent($content, $isActive), 'media' => $this->loadMediaContent($content, $isActive), 'slide' => $this->loadSlideContent($content, $isActive), default => null, }; } /** * @param array $content */ private function loadVideoContent(array $content, bool $isActive): void { $this->videoFilename = $content['filename'] ?? ''; $this->videoTitle = $content['title'] ?? ''; $this->videoPosition = $content['position'] ?? 25; $this->videoIsActive = $isActive; } /** * @param array $content */ private function loadFooterContent(array $content, bool $isActive): void { $this->footerHeadline = $content['headline'] ?? ''; $this->footerSubline = $content['subline'] ?? ''; $this->footerUrl = $content['url'] ?? ''; $this->footerIsActive = $isActive; } /** * @param array $content */ private function loadMediaContent(array $content, bool $isActive): void { $this->mediaType = $content['media_type'] ?? 'image'; $this->mediaCategory = $content['category'] ?? 'immobilien'; $this->mediaUrl = $content['media_url'] ?? ''; $this->mediaHeadline = $content['headline'] ?? ''; $this->mediaSubline = $content['subline'] ?? ''; $this->mediaDuration = $content['duration_seconds'] ?? 10; $this->mediaIsActive = $isActive; } /** * @param array $content */ private function loadSlideContent(array $content, bool $isActive): void { $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'] ?? ''; $this->slideEyebrow = $content['eyebrow'] ?? ''; $this->slideTitle = $content['title'] ?? ''; $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->slideBrandTagline = $content['brand_tagline'] ?? ''; $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'] ?? '') !== ''; } /** * @return array */ private function buildItemContent(): array { return match ($this->itemType) { 'video' => [ 'filename' => $this->videoFilename, 'title' => $this->videoTitle, 'position' => $this->videoPosition, ], 'footer' => [ 'headline' => $this->footerHeadline, 'subline' => $this->footerSubline, 'url' => $this->footerUrl ?: null, ], 'media' => [ 'media_type' => $this->mediaType, 'category' => $this->mediaCategory, 'media_url' => $this->mediaUrl, 'headline' => $this->mediaHeadline, 'subline' => $this->mediaSubline, 'duration_seconds' => $this->mediaDuration, ], 'slide' => [ '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, '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_contact' => $this->slideShowContact, 'brand_tagline' => $this->slideBrandTagline, ], default => [], }; } private function getActiveFlag(): bool { return match ($this->itemType) { 'video' => $this->videoIsActive, 'footer' => $this->footerIsActive, 'media' => $this->mediaIsActive, 'slide' => $this->slideIsActive, default => true, }; } private function defaultItemType(): string { return match ($this->version->type) { DisplayVersionType::VideoDisplay => 'video', DisplayVersionType::B2in => 'media', DisplayVersionType::Offers => 'slide', }; } private function resetItemForm(): void { $this->itemId = null; $this->itemType = ''; $this->videoFilename = ''; $this->videoTitle = ''; $this->videoPosition = 25; $this->videoIsActive = true; $this->footerHeadline = ''; $this->footerSubline = ''; $this->footerUrl = ''; $this->footerIsActive = true; $this->mediaType = 'image'; $this->mediaCategory = 'immobilien'; $this->mediaUrl = ''; $this->mediaHeadline = ''; $this->mediaSubline = ''; $this->mediaDuration = 10; $this->mediaIsActive = true; $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->slideShowContact = true; $this->slideBrandTagline = ''; $this->slideIsActive = true; } private function refreshModulePreview(): void { $this->previewFrameRefreshCounter++; } /** * @return array */ private function settingsWithDefaults(): array { return DisplayModuleSettings::merge($this->version->type, $this->version->settings); } public function render() { $items = $this->version->items()->get()->groupBy('item_type'); return view('livewire.admin.cms.display-version-editor', [ 'items' => $items, ]); } }