10-04-2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:18:17 +02:00
parent 4d6b4930b2
commit 4bb89aad8c
836 changed files with 52961 additions and 5950 deletions

View file

@ -0,0 +1,302 @@
<?php
namespace App\Livewire\Admin\Cms;
use App\Models\DisplayFooterContent;
use App\Models\DisplayVideo;
use Illuminate\Support\Facades\File;
use Livewire\Component;
class CabinetDisplay extends Component
{
// Video-Verwaltung
public $videoId = null;
public $videoFilename = '';
public $videoTitle = '';
public $videoPosition = 25;
public $videoIsActive = true;
public $showVideoModal = false;
public $availableVideos = [];
// Footer-Content-Verwaltung
public $footerId = null;
public $footerHeadline = '';
public $footerSubline = '';
public $footerUrl = '';
public $footerIsActive = true;
public $showFooterModal = false;
public function mount()
{
$this->loadAvailableVideos();
}
/**
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
*/
public function loadAvailableVideos()
{
$assetsPath = public_path('_cabinet/assets');
if (File::exists($assetsPath)) {
$files = File::files($assetsPath);
$this->availableVideos = collect($files)
->map(fn ($file) => $file->getFilename())
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
->values()
->toArray();
}
}
// ========================================
// VIDEO-VERWALTUNG
// ========================================
public function openVideoModal($id = null)
{
if ($id) {
$video = DisplayVideo::findOrFail($id);
$this->videoId = $video->id;
$this->videoFilename = $video->filename;
$this->videoTitle = $video->title ?? '';
$this->videoPosition = $video->position;
$this->videoIsActive = $video->is_active;
} else {
$this->resetVideoForm();
}
$this->showVideoModal = true;
}
public function saveVideo()
{
$this->validate([
'videoFilename' => 'required|string',
'videoPosition' => 'required|integer|min:0|max:100',
], [
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
'videoPosition.required' => 'Die Position ist erforderlich.',
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
]);
$data = [
'filename' => $this->videoFilename,
'title' => $this->videoTitle,
'position' => $this->videoPosition,
'is_active' => $this->videoIsActive,
];
if ($this->videoId) {
$video = DisplayVideo::findOrFail($this->videoId);
$video->update($data);
session()->flash('success', 'Video erfolgreich aktualisiert!');
} else {
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
$data['sort_order'] = $maxSortOrder + 1;
DisplayVideo::create($data);
session()->flash('success', 'Video erfolgreich hinzugefügt!');
}
$this->closeVideoModal();
}
public function deleteVideo($id)
{
DisplayVideo::findOrFail($id)->delete();
session()->flash('success', 'Video erfolgreich gelöscht!');
}
public function toggleVideoStatus($id)
{
$video = DisplayVideo::findOrFail($id);
$video->update(['is_active' => ! $video->is_active]);
}
public function moveVideo($id, $direction)
{
$video = DisplayVideo::findOrFail($id);
$currentOrder = $video->sort_order;
if ($direction === 'up' && $currentOrder > 0) {
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
if ($swapVideo) {
$video->update(['sort_order' => $currentOrder - 1]);
$swapVideo->update(['sort_order' => $currentOrder]);
}
} elseif ($direction === 'down') {
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
if ($swapVideo) {
$video->update(['sort_order' => $currentOrder + 1]);
$swapVideo->update(['sort_order' => $currentOrder]);
}
}
}
public function resetVideoForm()
{
$this->videoId = null;
$this->videoFilename = '';
$this->videoTitle = '';
$this->videoPosition = 25;
$this->videoIsActive = true;
}
public function closeVideoModal()
{
$this->showVideoModal = false;
$this->resetVideoForm();
}
// ========================================
// FOOTER-CONTENT-VERWALTUNG
// ========================================
public function openFooterModal($id = null)
{
if ($id) {
$footer = DisplayFooterContent::findOrFail($id);
$this->footerId = $footer->id;
$this->footerHeadline = $footer->headline;
$this->footerSubline = $footer->subline;
$this->footerUrl = $footer->url;
$this->footerIsActive = $footer->is_active;
} else {
$this->resetFooterForm();
}
$this->showFooterModal = true;
}
public function saveFooter()
{
$this->validate([
'footerHeadline' => 'required|string|max:255',
'footerSubline' => 'required|string|max:255',
'footerUrl' => 'nullable|url',
], [
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
]);
$data = [
'headline' => $this->footerHeadline,
'subline' => $this->footerSubline,
'url' => $this->footerUrl ?: null,
'is_active' => $this->footerIsActive,
];
if ($this->footerId) {
$footer = DisplayFooterContent::findOrFail($this->footerId);
$footer->update($data);
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
if ($footer->url && ! $footer->short_code) {
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
}
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
} else {
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
$data['sort_order'] = $maxSortOrder + 1;
$footer = DisplayFooterContent::create($data);
// Short-Code nur generieren wenn URL vorhanden
if ($footer->url) {
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: '.$footer->short_url);
} else {
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
}
}
$this->closeFooterModal();
}
public function regenerateShortCode($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
session()->flash('success', 'Short-Code wurde neu generiert!');
}
public function resetClicks($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->clicks = 0;
$footer->save();
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
}
public function deleteFooter($id)
{
DisplayFooterContent::findOrFail($id)->delete();
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
}
public function toggleFooterStatus($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->update(['is_active' => ! $footer->is_active]);
}
public function moveFooter($id, $direction)
{
$footer = DisplayFooterContent::findOrFail($id);
$currentOrder = $footer->sort_order;
if ($direction === 'up' && $currentOrder > 0) {
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
if ($swapFooter) {
$footer->update(['sort_order' => $currentOrder - 1]);
$swapFooter->update(['sort_order' => $currentOrder]);
}
} elseif ($direction === 'down') {
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
if ($swapFooter) {
$footer->update(['sort_order' => $currentOrder + 1]);
$swapFooter->update(['sort_order' => $currentOrder]);
}
}
}
public function resetFooterForm()
{
$this->footerId = null;
$this->footerHeadline = '';
$this->footerSubline = '';
$this->footerUrl = '';
$this->footerIsActive = true;
}
public function closeFooterModal()
{
$this->showFooterModal = false;
$this->resetFooterForm();
}
public function render()
{
$videos = DisplayVideo::orderBy('sort_order')->get();
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
return view('livewire.admin.cms.cabinet-display', [
'videos' => $videos,
'footerContents' => $footerContents,
]);
}
}

View file

@ -0,0 +1,189 @@
<?php
namespace App\Livewire\Admin\Cms;
use App\Models\CabinetTabletSetting;
use Livewire\Component;
class CabinetInfoTablet extends Component
{
// Store status mode
public string $storeStatus = 'auto';
public string $noticeHeadline = '';
public string $noticeSubtext = '';
// Override times for today
public ?string $overrideOpenToday = '';
public ?string $overrideCloseToday = '';
// Appointment
public ?string $nextAppointmentDate = null;
public ?string $nextAppointmentTime = '';
// Structured opening hours per weekday (open + close, empty = closed)
public ?string $hoursMondayOpen = '10:00';
public ?string $hoursMondayClose = '18:00';
public ?string $hoursTuesdayOpen = '10:00';
public ?string $hoursTuesdayClose = '18:00';
public ?string $hoursWednesdayOpen = '10:00';
public ?string $hoursWednesdayClose = '18:00';
public ?string $hoursThursdayOpen = '10:00';
public ?string $hoursThursdayClose = '18:00';
public ?string $hoursFridayOpen = '10:00';
public ?string $hoursFridayClose = '18:00';
public ?string $hoursSaturdayOpen = '10:00';
public ?string $hoursSaturdayClose = '14:00';
public ?string $hoursSundayOpen = '';
public ?string $hoursSundayClose = '';
// Contact
public string $contactPhone = '';
public string $contactEmail = '';
public function mount(): void
{
$s = CabinetTabletSetting::current();
$this->storeStatus = $s->store_status ?? 'auto';
$this->noticeHeadline = $s->notice_headline ?? '';
$this->noticeSubtext = $s->notice_subtext ?? '';
$this->overrideOpenToday = $s->override_open_today ?? '';
$this->overrideCloseToday = $s->override_close_today ?? '';
$this->nextAppointmentDate = $s->next_appointment_date?->format('Y-m-d');
$this->nextAppointmentTime = $s->next_appointment_time ?? '';
$this->hoursMondayOpen = $s->hours_monday_open ?? '';
$this->hoursMondayClose = $s->hours_monday_close ?? '';
$this->hoursTuesdayOpen = $s->hours_tuesday_open ?? '';
$this->hoursTuesdayClose = $s->hours_tuesday_close ?? '';
$this->hoursWednesdayOpen = $s->hours_wednesday_open ?? '';
$this->hoursWednesdayClose = $s->hours_wednesday_close ?? '';
$this->hoursThursdayOpen = $s->hours_thursday_open ?? '';
$this->hoursThursdayClose = $s->hours_thursday_close ?? '';
$this->hoursFridayOpen = $s->hours_friday_open ?? '';
$this->hoursFridayClose = $s->hours_friday_close ?? '';
$this->hoursSaturdayOpen = $s->hours_saturday_open ?? '';
$this->hoursSaturdayClose = $s->hours_saturday_close ?? '';
$this->hoursSundayOpen = $s->hours_sunday_open ?? '';
$this->hoursSundayClose = $s->hours_sunday_close ?? '';
$this->contactPhone = $s->contact_phone ?? '';
$this->contactEmail = $s->contact_email ?? '';
}
private function timeRule(): array
{
return ['nullable', 'string', 'regex:/^(\d{2}:\d{2})?$/'];
}
private function toNullIfEmpty(?string $value): ?string
{
return $value !== null && trim($value) !== '' ? trim($value) : null;
}
/**
* @param array<string, string> $hours Optional: Time picker values from DOM (bypasses wire:model sync issues)
*/
public function save(array $hours = []): void
{
foreach ($hours as $prop => $value) {
if (property_exists($this, $prop)) {
$this->{$prop} = $value ?? '';
}
}
$timeRule = $this->timeRule();
$this->validate([
'storeStatus' => 'required|in:auto,notice,warning,closed',
'noticeHeadline' => 'nullable|string|max:40',
'noticeSubtext' => 'nullable|string|max:80',
'overrideOpenToday' => $timeRule,
'overrideCloseToday' => $timeRule,
'nextAppointmentDate' => 'nullable|date',
'nextAppointmentTime' => $timeRule,
'hoursMondayOpen' => $timeRule,
'hoursMondayClose' => $timeRule,
'hoursTuesdayOpen' => $timeRule,
'hoursTuesdayClose' => $timeRule,
'hoursWednesdayOpen' => $timeRule,
'hoursWednesdayClose' => $timeRule,
'hoursThursdayOpen' => $timeRule,
'hoursThursdayClose' => $timeRule,
'hoursFridayOpen' => $timeRule,
'hoursFridayClose' => $timeRule,
'hoursSaturdayOpen' => $timeRule,
'hoursSaturdayClose' => $timeRule,
'hoursSundayOpen' => $timeRule,
'hoursSundayClose' => $timeRule,
'contactPhone' => 'nullable|string|max:50',
'contactEmail' => 'nullable|email|max:100',
], [
'storeStatus.required' => 'Der Store-Status ist erforderlich.',
'storeStatus.in' => 'Ungültiger Status. Erlaubt: auto, notice, warning, closed.',
'noticeHeadline.max' => 'Die Headline darf maximal 40 Zeichen haben.',
'noticeSubtext.max' => 'Der Subtext darf maximal 80 Zeichen haben.',
'overrideOpenToday.regex' => 'Bitte im Format HH:MM eingeben.',
'overrideCloseToday.regex' => 'Bitte im Format HH:MM eingeben.',
'nextAppointmentTime.regex' => 'Bitte im Format HH:MM eingeben.',
'contactEmail.email' => 'Bitte eine gültige E-Mail-Adresse eingeben.',
]);
CabinetTabletSetting::current()->update([
'store_status' => $this->storeStatus,
'notice_headline' => $this->toNullIfEmpty($this->noticeHeadline),
'notice_subtext' => $this->toNullIfEmpty($this->noticeSubtext),
'override_open_today' => $this->toNullIfEmpty($this->overrideOpenToday),
'override_close_today' => $this->toNullIfEmpty($this->overrideCloseToday),
'next_appointment_date' => $this->toNullIfEmpty($this->nextAppointmentDate),
'next_appointment_time' => $this->toNullIfEmpty($this->nextAppointmentTime),
'hours_monday_open' => $this->toNullIfEmpty($this->hoursMondayOpen),
'hours_monday_close' => $this->toNullIfEmpty($this->hoursMondayClose),
'hours_tuesday_open' => $this->toNullIfEmpty($this->hoursTuesdayOpen),
'hours_tuesday_close' => $this->toNullIfEmpty($this->hoursTuesdayClose),
'hours_wednesday_open' => $this->toNullIfEmpty($this->hoursWednesdayOpen),
'hours_wednesday_close' => $this->toNullIfEmpty($this->hoursWednesdayClose),
'hours_thursday_open' => $this->toNullIfEmpty($this->hoursThursdayOpen),
'hours_thursday_close' => $this->toNullIfEmpty($this->hoursThursdayClose),
'hours_friday_open' => $this->toNullIfEmpty($this->hoursFridayOpen),
'hours_friday_close' => $this->toNullIfEmpty($this->hoursFridayClose),
'hours_saturday_open' => $this->toNullIfEmpty($this->hoursSaturdayOpen),
'hours_saturday_close' => $this->toNullIfEmpty($this->hoursSaturdayClose),
'hours_sunday_open' => $this->toNullIfEmpty($this->hoursSundayOpen),
'hours_sunday_close' => $this->toNullIfEmpty($this->hoursSundayClose),
'contact_phone' => $this->toNullIfEmpty($this->contactPhone),
'contact_email' => $this->toNullIfEmpty($this->contactEmail),
]);
session()->flash('success', 'Info-Tablet Einstellungen gespeichert!');
}
public function clearOverrides(): void
{
CabinetTabletSetting::current()->clearOverrides();
$this->overrideOpenToday = '';
$this->overrideCloseToday = '';
session()->flash('success', 'Sonderöffnungszeiten wurden zurückgesetzt!');
}
public function render(): \Illuminate\View\View
{
return view('livewire.admin.cms.cabinet-info-tablet');
}
}

View file

@ -0,0 +1,153 @@
<?php
namespace App\Livewire\Admin\Cms;
use App\Models\Display;
use App\Models\DisplayVersion;
use Livewire\Component;
class DisplayList extends Component
{
public $showModal = false;
public $displayId = null;
public $displayName = '';
public $displayLocation = '';
/** @var array<int> */
public $selectedVersionIds = [];
public $displayIsActive = true;
public $addVersionSelect = null;
public function openModal(?int $id = null): void
{
if ($id) {
$display = Display::with('versions')->findOrFail($id);
$this->displayId = $display->id;
$this->displayName = $display->name;
$this->displayLocation = $display->location ?? '';
$this->selectedVersionIds = $display->versions->pluck('id')->toArray();
$this->displayIsActive = $display->is_active;
} else {
$this->resetForm();
}
$this->showModal = true;
}
public function addVersion(?int $versionId = null): void
{
$id = $versionId ?? $this->addVersionSelect;
if ($id && ! in_array((int) $id, $this->selectedVersionIds)) {
$this->selectedVersionIds[] = (int) $id;
}
$this->addVersionSelect = null;
}
public function removeVersion(int $index): void
{
array_splice($this->selectedVersionIds, $index, 1);
}
public function moveVersion(int $index, string $direction): void
{
$newIndex = $direction === 'up' ? $index - 1 : $index + 1;
if ($newIndex < 0 || $newIndex >= count($this->selectedVersionIds)) {
return;
}
$temp = $this->selectedVersionIds[$index];
$this->selectedVersionIds[$index] = $this->selectedVersionIds[$newIndex];
$this->selectedVersionIds[$newIndex] = $temp;
}
public function save(): void
{
$this->validate([
'displayName' => 'required|string|max:255',
'displayLocation' => 'nullable|string|max:255',
'selectedVersionIds' => 'array',
'selectedVersionIds.*' => 'exists:display_versions,id',
], [
'displayName.required' => 'Bitte geben Sie einen Namen ein.',
]);
$data = [
'name' => $this->displayName,
'location' => $this->displayLocation ?: null,
'is_active' => $this->displayIsActive,
];
if ($this->displayId) {
$display = Display::findOrFail($this->displayId);
$display->update($data);
session()->flash('success', 'Display erfolgreich aktualisiert!');
} else {
$display = Display::create($data);
session()->flash('success', 'Display erfolgreich erstellt!');
}
// Sync versions with sort_order
$syncData = [];
foreach ($this->selectedVersionIds as $sortOrder => $versionId) {
$syncData[$versionId] = ['sort_order' => $sortOrder];
}
$display->versions()->sync($syncData);
$this->closeModal();
}
public function deleteDisplay(int $id): void
{
$display = Display::findOrFail($id);
$name = $display->name;
$display->delete();
session()->flash('success', 'Display "'.$name.'" wurde gelöscht!');
}
public function toggleActive(int $id): void
{
$display = Display::findOrFail($id);
$display->update(['is_active' => ! $display->is_active]);
}
public function closeModal(): void
{
$this->showModal = false;
$this->resetForm();
}
public function resetForm(): void
{
$this->displayId = null;
$this->displayName = '';
$this->displayLocation = '';
$this->selectedVersionIds = [];
$this->displayIsActive = true;
$this->addVersionSelect = null;
}
public function render()
{
$displays = Display::with('versions')
->orderBy('name')
->get();
$versions = DisplayVersion::active()
->orderBy('name')
->get();
return view('livewire.admin.cms.display-list', [
'displays' => $displays,
'versions' => $versions,
]);
}
}

View file

@ -0,0 +1,437 @@
<?php
namespace App\Livewire\Admin\Cms;
use App\Enums\DisplayVersionType;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use Illuminate\Support\Facades\File;
use Livewire\Component;
class DisplayVersionEditor extends Component
{
public DisplayVersion $version;
public string $versionName = '';
// Item Modal
public bool $showItemModal = false;
public ?int $itemId = null;
public string $itemType = '';
// Video-Display: Video fields
public string $videoFilename = '';
public string $videoTitle = '';
public int $videoPosition = 25;
public bool $videoIsActive = true;
// Video-Display: Footer fields
public string $footerHeadline = '';
public string $footerSubline = '';
public string $footerUrl = '';
public bool $footerIsActive = true;
// B2in: Media fields
public string $mediaType = 'image';
public string $mediaCategory = 'immobilien';
public string $mediaUrl = '';
public string $mediaHeadline = '';
public string $mediaSubline = '';
public int $mediaDuration = 10;
public bool $mediaIsActive = true;
// Offers: Slide fields
public string $slideType = 'product-hero';
public int $slideDuration = 8000;
public string $slideImageUrl = '';
public string $slideBadge = '';
public string $slideEyebrow = '';
public string $slideTitle = '';
public string $slideSubline = '';
public string $slidePrice = '';
public string $slideOriginalPrice = '';
public string $slideTagText = '';
/** @var array<string> */
public array $slideBullets = [];
public string $slideDisclaimer = '';
public string $slideQrUrl = '';
public string $slideQrTitle = '';
public string $slideContact = '';
public bool $slideShowBrandText = false;
public string $slideBrandTagline = '';
public bool $slideIsActive = true;
// Settings Modal
public bool $showSettingsModal = false;
public array $settings = [];
/** @var array<string> */
public array $availableVideos = [];
public function mount(DisplayVersion $displayVersion): void
{
$this->version = $displayVersion;
$this->versionName = $displayVersion->name;
$this->settings = $displayVersion->settings ?? [];
if ($this->version->type === DisplayVersionType::VideoDisplay) {
$this->loadAvailableVideos();
}
}
public function loadAvailableVideos(): 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();
}
}
public function toggleTheme(): void
{
$settings = $this->version->settings ?? [];
$settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark';
$this->version->update(['settings' => $settings]);
$this->settings = $settings;
}
public function saveName(): void
{
$this->validate([
'versionName' => 'required|string|max:255',
]);
$this->version->update(['name' => $this->versionName]);
session()->flash('success', 'Name aktualisiert!');
}
// ========================================
// SETTINGS
// ========================================
public function openSettingsModal(): void
{
$this->settings = $this->version->settings ?? [];
$this->showSettingsModal = true;
}
public function saveSettings(): void
{
$this->version->update(['settings' => $this->settings]);
$this->showSettingsModal = false;
session()->flash('success', 'Einstellungen gespeichert!');
}
// ========================================
// 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;
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->closeItemModal();
}
public function deleteItem(int $id): void
{
DisplayVersionItem::findOrFail($id)->delete();
session()->flash('success', 'Inhalt gelöscht!');
}
public function toggleItemStatus(int $id): void
{
$item = DisplayVersionItem::findOrFail($id);
$item->update(['is_active' => ! $item->is_active]);
}
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]);
}
}
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;
match ($item->item_type) {
'video' => $this->loadVideoContent($content),
'footer' => $this->loadFooterContent($content),
'media' => $this->loadMediaContent($content),
'slide' => $this->loadSlideContent($content),
default => null,
};
}
private function loadVideoContent(array $content): void
{
$this->videoFilename = $content['filename'] ?? '';
$this->videoTitle = $content['title'] ?? '';
$this->videoPosition = $content['position'] ?? 25;
$this->videoIsActive = true;
}
private function loadFooterContent(array $content): void
{
$this->footerHeadline = $content['headline'] ?? '';
$this->footerSubline = $content['subline'] ?? '';
$this->footerUrl = $content['url'] ?? '';
$this->footerIsActive = true;
}
private function loadMediaContent(array $content): 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 = true;
}
private function loadSlideContent(array $content): void
{
$this->slideType = $content['type'] ?? 'product-hero';
$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->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;
}
/**
* @return array<string, mixed>
*/
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' => $this->slideType,
'duration' => $this->slideDuration,
'image_url' => $this->slideImageUrl,
'badge_text' => $this->slideBadge,
'eyebrow' => $this->slideEyebrow,
'title' => $this->slideTitle,
'subline' => $this->slideSubline,
'price' => $this->slidePrice,
'original_price' => $this->slideOriginalPrice,
'tag_text' => $this->slideTagText,
'bullets' => $this->slideBullets,
'disclaimer' => $this->slideDisclaimer,
'qr_url' => $this->slideQrUrl,
'qr_title' => $this->slideQrTitle,
'contact' => $this->slideContact,
'show_brand_text' => $this->slideShowBrandText,
'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->slideType = 'product-hero';
$this->slideDuration = 8000;
$this->slideImageUrl = '';
$this->slideBadge = '';
$this->slideEyebrow = '';
$this->slideTitle = '';
$this->slideSubline = '';
$this->slidePrice = '';
$this->slideOriginalPrice = '';
$this->slideTagText = '';
$this->slideBullets = [];
$this->slideDisclaimer = '';
$this->slideQrUrl = '';
$this->slideQrTitle = '';
$this->slideContact = '';
$this->slideShowBrandText = false;
$this->slideBrandTagline = '';
$this->slideIsActive = true;
}
public function render()
{
$items = $this->version->items()->get()->groupBy('item_type');
return view('livewire.admin.cms.display-version-editor', [
'items' => $items,
]);
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace App\Livewire\Admin\Cms;
use App\Enums\DisplayVersionType;
use App\Models\DisplayVersion;
use Livewire\Component;
class DisplayVersionList extends Component
{
public $showCreateModal = false;
public $newName = '';
public $newType = '';
public function openCreateModal(): void
{
$this->newName = '';
$this->newType = '';
$this->showCreateModal = true;
}
public function createVersion(): void
{
$this->validate([
'newName' => 'required|string|max:255',
'newType' => 'required|string|in:video-display,b2in,offers',
], [
'newName.required' => 'Bitte geben Sie einen Namen ein.',
'newType.required' => 'Bitte wählen Sie einen Typ aus.',
]);
$version = DisplayVersion::create([
'name' => $this->newName,
'type' => $this->newType,
'settings' => $this->defaultSettingsForType($this->newType),
'is_active' => true,
]);
$this->showCreateModal = false;
$this->newName = '';
$this->newType = '';
session()->flash('success', 'Version "'.$version->name.'" wurde erstellt!');
$this->redirect(
route('admin.cms.display-version-edit', $version),
navigate: true
);
}
public function deleteVersion(int $id): void
{
$version = DisplayVersion::findOrFail($id);
$name = $version->name;
$version->delete();
session()->flash('success', 'Version "'.$name.'" wurde gelöscht!');
}
public function toggleActive(int $id): void
{
$version = DisplayVersion::findOrFail($id);
$version->update(['is_active' => ! $version->is_active]);
}
/**
* @return array<string, mixed>
*/
private function defaultSettingsForType(string $type): array
{
return match ($type) {
'b2in' => [
'theme' => 'dark',
'footer_name' => '',
'footer_url' => '',
'transition' => ['type' => 'crossfade', 'duration_ms' => 800],
'default_image_duration' => 10,
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
'display_active' => true,
],
'offers' => [
'loop' => true,
'transition' => ['type' => 'fade', 'duration' => 600],
],
default => [],
};
}
public function render()
{
$versions = DisplayVersion::withCount(['items', 'displays'])
->orderBy('name')
->get();
return view('livewire.admin.cms.display-version-list', [
'versions' => $versions,
'types' => DisplayVersionType::cases(),
]);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Admin\Cms;
use FluxCms\Core\Services\MediaConversionService;
use Livewire\Component;
use Livewire\WithFileUploads;
class MediaLibraryUploader extends Component
{
use WithFileUploads;
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
public array $uploads = [];
public function updatedUploads(): void
{
$this->validate([
'uploads' => 'nullable|array|max:20',
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
]);
$service = app(MediaConversionService::class);
foreach ($this->uploads as $file) {
$media = $service->storeUpload($file);
$this->dispatch('media-library-uploaded', mediaId: $media->id);
}
$this->uploads = [];
}
public function removeUpload(int $index): void
{
if (isset($this->uploads[$index])) {
unset($this->uploads[$index]);
$this->uploads = array_values($this->uploads);
}
}
public function render()
{
return view('livewire.admin.cms.media-library-uploader');
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace App\Livewire\Admin\Cms;
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\MediaConversionService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\View\View;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\WithPagination;
class MediaPicker extends Component
{
use WithFileUploads;
use WithPagination;
public ?int $value = null;
public string $field = 'media_id';
public string $type = 'image';
public string $profile = 'thumbnail';
public string $label = 'Bild auswählen';
public bool $showModal = false;
public string $search = '';
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
public array $quickUploads = [];
public function mount(?int $value = null): void
{
$this->value = $value;
}
public function openPicker(): void
{
$this->showModal = true;
}
public function selectMedia(int $id): void
{
$media = CmsMedia::find($id);
if (! $media) {
return;
}
if ($media->isImage() && $this->profile) {
$service = app(MediaConversionService::class);
if (! $media->hasConversion($this->profile)) {
$service->convert($media, $this->profile);
$media->refresh();
}
}
$this->value = $media->id;
$this->showModal = false;
$this->dispatch('media-selected', field: $this->field, mediaId: $media->id, url: $media->getConversionUrl($this->profile));
}
public function clearSelection(): void
{
$this->value = null;
$this->dispatch('media-selected', field: $this->field, mediaId: null, url: null);
}
public function updatedQuickUploads(): void
{
$this->validate([
'quickUploads' => 'nullable|array|max:5',
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
]);
$service = app(MediaConversionService::class);
$lastMedia = null;
foreach ($this->quickUploads as $file) {
$lastMedia = $service->storeUpload($file);
if ($lastMedia->isImage() && $this->profile) {
$service->convert($lastMedia, $this->profile);
$lastMedia->refresh();
}
}
$this->quickUploads = [];
if ($lastMedia) {
$this->value = $lastMedia->id;
$this->showModal = false;
$this->dispatch('media-selected', field: $this->field, mediaId: $lastMedia->id, url: $lastMedia->getConversionUrl($this->profile));
}
}
public function removeQuickUpload(int $index): void
{
if (isset($this->quickUploads[$index])) {
unset($this->quickUploads[$index]);
$this->quickUploads = array_values($this->quickUploads);
}
}
public function render(): View
{
return view('livewire.admin.cms.media-picker', [
'selectedMedia' => $this->resolveSelectedMedia(),
'mediaItems' => $this->resolveMediaItems(),
]);
}
private function resolveSelectedMedia(): ?CmsMedia
{
if (! $this->value) {
return null;
}
return CmsMedia::find($this->value);
}
/**
* @return LengthAwarePaginator<int, CmsMedia>
*/
private function resolveMediaItems(): LengthAwarePaginator
{
return CmsMedia::query()
->when($this->type === 'image', fn ($q) => $q->images())
->when($this->type === 'pdf', fn ($q) => $q->pdfs())
->when($this->type === 'document', fn ($q) => $q->documents())
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
->orderByDesc('created_at')
->paginate(18);
}
}