b2in/resources/views/livewire/admin/cms/display-version-editor.blade.php
Kevin Adametz 6c6d683b9a 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>
2026-05-29 15:57:33 +00:00

390 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div>
{{-- Header --}}
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<flux:button :href="route('admin.cms.display-modules')" wire:navigate variant="ghost" icon="arrow-left" size="sm">
{{ __('Zurück') }}
</flux:button>
<div>
<div class="flex items-center gap-3">
<flux:heading size="xl">{{ $version->name }}</flux:heading>
<flux:badge color="{{ match($version->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}">
{{ $version->type->label() }}
</flux:badge>
</div>
<flux:subheading>{{ __('Modul bearbeiten') }}</flux:subheading>
</div>
</div>
<div class="flex items-center gap-3">
@if($version->type->value === 'b2in')
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700">
<flux:icon.sun class="w-4 h-4 text-amber-500" />
<flux:switch wire:click="toggleTheme"
:checked="($version->settings['theme'] ?? 'dark') === 'dark'" />
<flux:icon.moon class="w-4 h-4 text-indigo-400" />
</div>
@endif
<flux:button wire:click="openSettingsModal" icon="cog-6-tooth" variant="ghost">
{{ __('Einstellungen') }}
</flux:button>
</div>
</div>
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
{{-- Name bearbeiten --}}
<flux:card class="mb-6">
<form wire:submit.prevent="saveName" class="flex items-end gap-4">
<div class="flex-1">
<flux:input wire:model="versionName" label="Modulname" />
</div>
<flux:button type="submit" variant="primary" size="sm">{{ __('Speichern') }}</flux:button>
</form>
</flux:card>
{{-- Modul-Vorschau --}}
<flux:card class="mb-6">
<div class="mb-4 flex items-center justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('Modul-Vorschau') }}</flux:heading>
<flux:subheading>{{ __('Live gerenderte Einzelmodul-Vorschau im Display-Player') }}</flux:subheading>
</div>
<a href="{{ $this->modulePreviewUrl() }}"
target="_blank"
class="inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400">
<flux:icon.arrow-top-right-on-square class="w-3 h-3" />
{{ __('Vollbild-Vorschau') }}
</a>
</div>
<div class="mx-auto aspect-[9/16] w-full max-w-[320px] overflow-hidden rounded-xl border border-zinc-200 bg-black shadow-sm dark:border-zinc-700">
<iframe
wire:key="module-preview-{{ $previewFrameRefreshCounter }}"
src="{{ $this->modulePreviewUrl() }}"
class="h-full w-full border-0"
loading="lazy"
title="{{ __('Modul-Vorschau') }}"
></iframe>
</div>
</flux:card>
{{-- Type-specific content sections --}}
@if($version->type->value === 'video-display')
@include('livewire.admin.cms.partials.version-editor-video', ['items' => $items])
@elseif($version->type->value === 'b2in')
@include('livewire.admin.cms.partials.version-editor-b2in', ['items' => $items])
@elseif($version->type->value === 'offers')
@include('livewire.admin.cms.partials.version-editor-offers', ['items' => $items])
@endif
{{-- Module-level metadata --}}
<flux:card class="mt-8 mb-6">
<form wire:submit.prevent="saveSettings">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Meta-Einstellungen für dieses Modul') }}</flux:heading>
<flux:subheading>{{ __('Diese Werte gelten für die gesamte Media-Playlist bzw. alle Slides dieses Moduls.') }}</flux:subheading>
</div>
@include('livewire.admin.cms.partials.version-editor-settings', ['context' => 'inline'])
<div class="flex justify-end">
<flux:button type="submit" variant="primary" size="sm">
{{ __('Meta-Einstellungen speichern') }}
</flux:button>
</div>
</div>
</form>
</flux:card>
{{-- Item Modal --}}
<flux:modal :open="$showItemModal" wire:model="showItemModal" class="w-full max-w-5xl">
<form wire:submit.prevent="saveItem">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ $itemId ? __('Inhalt bearbeiten') : __('Inhalt hinzufügen') }}</flux:heading>
</div>
{{-- Video fields --}}
@if($itemType === 'video')
<livewire:admin.cms.display-media-picker
:value="null"
field="videoFilename"
type="video"
label="Video aus Mediathek"
:key="'picker-video-' . ($itemId ?? 'new')" />
<div class="flex items-end gap-3">
<x-media-thumb :url="$videoFilename" />
<flux:input wire:model.live.debounce.500ms="videoFilename" label="Video-Pfad / URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt Pfad/URL eingeben." class="flex-1" />
</div>
<flux:input wire:model="videoTitle" label="Name" placeholder="z.B. Herbst Kollektion 2025"
description="Interner Name des Videos wird nicht im Video eingeblendet." />
<flux:input wire:model="videoPosition" type="number" min="0" max="100" label="Position (%)"
description="Vertikale Position im Video (0 = oben, 100 = unten)" />
<flux:checkbox wire:model="videoIsActive" label="Aktiv" />
@endif
{{-- Footer fields --}}
@if($itemType === 'footer')
<flux:input wire:model="footerHeadline" label="Überschrift" placeholder="z.B. Beratung & Termin" />
<flux:input wire:model="footerSubline" label="Unterzeile" placeholder="z.B. Jetzt Termin vereinbaren." />
<flux:input wire:model="footerUrl" label="URL (optional)" placeholder="https://..."
description="Leer = kein QR-Code. Mit URL = QR-Code wird generiert." />
<flux:checkbox wire:model="footerIsActive" label="Aktiv" />
@endif
{{-- Media fields (B2in) --}}
@if($itemType === 'media')
<livewire:admin.cms.display-media-picker
:value="null"
field="mediaUrl"
type="all"
label="Aus Mediathek"
:key="'picker-media-' . ($itemId ?? 'new')" />
<div class="flex items-end gap-3">
<x-media-thumb :url="$mediaUrl" />
<flux:input wire:model.live.debounce.500ms="mediaUrl" label="Medien-URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt URL eingeben." class="flex-1" />
</div>
<div class="flex items-center gap-2">
<flux:text class="text-sm">{{ __('Medientyp:') }}</flux:text>
<flux:badge size="sm" :color="$mediaType === 'video' ? 'purple' : 'sky'">
{{ $mediaType === 'video' ? __('Video') : __('Bild') }}
</flux:badge>
<flux:text class="text-xs text-zinc-400">{{ __('wird automatisch aus dem gewählten Medium erkannt') }}</flux:text>
</div>
<flux:select wire:model="mediaCategory" label="Kategorie">
<option value="immobilien">Immobilien</option>
<option value="moebel">Möbel</option>
<option value="sonstiges">Sonstiges</option>
</flux:select>
<flux:input wire:model="mediaHeadline" label="Überschrift" placeholder="z.B. Ihr Zuhause. Weltweit." />
<flux:input wire:model="mediaSubline" label="Unterzeile" placeholder="z.B. Beratung und Vermittlung." />
@if($mediaType === 'image')
<flux:input wire:model="mediaDuration" type="number" min="1" max="120" label="Dauer (Sekunden)"
description="Nur für Bilder Videos spielen bis zum Ende." />
@endif
<flux:checkbox wire:model="mediaIsActive" label="Aktiv" />
@endif
{{-- Slide fields (Offers) einheitliches Detail-Layout mit Ein-/Ausblende-Schaltern --}}
@if($itemType === 'slide')
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">
{{ __('Jedes Angebot nutzt dasselbe Detail-Layout. Über die Schalter blendest du einzelne Bausteine ein oder aus und befüllst sie mit Inhalten.') }}
</flux:text>
{{-- Logo & Marke (Kopfbereich) --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Logo & Marke') }}</flux:heading>
<flux:switch wire:model.live="slideShowLogo" label="Logo & Marken-Text anzeigen"
description="Kopfbereich oben mit Logo, Marken-Text und Tagline." />
@if($slideShowLogo)
<livewire:admin.cms.display-media-picker
:value="null"
field="slideLogoUrl"
type="image"
label="Logo aus Mediathek"
:key="'picker-slide-logo-' . ($itemId ?? 'new')" />
<div class="flex items-end gap-3">
<x-media-thumb :url="$slideLogoUrl" />
<flux:input wire:model.live.debounce.500ms="slideLogoUrl" label="Logo-URL" placeholder="Standard: CABINET-Logo"
description="Leer = Standard-Logo wird genutzt." class="flex-1" />
</div>
<flux:input wire:model="slideBrandText" label="Marken-Text" placeholder="z.B. Bielefeld"
description="Text direkt neben dem Logo (optional)." />
<flux:input wire:model="slideBrandTagline" label="Tagline" placeholder="z.B. Planung • Beratung • Lieferung & Montage"
description="Rechts im Kopfbereich (optional)." />
@endif
</div>
{{-- Bild & Badge (wichtigstes Element farblich hervorgehoben) --}}
<div class="space-y-4 rounded-xl border-2 border-blue-300 bg-blue-50/60 p-4 ring-1 ring-blue-200 dark:border-blue-700 dark:bg-blue-950/30 dark:ring-blue-900">
<div class="flex items-center gap-2">
<flux:heading size="sm">{{ __('Bild & Badge') }}</flux:heading>
<flux:badge color="blue" size="sm">{{ __('Wichtigstes Element') }}</flux:badge>
</div>
<livewire:admin.cms.display-media-picker
:value="null"
field="slideImageUrl"
type="image"
label="Bild aus Mediathek"
:key="'picker-slide-' . ($itemId ?? 'new')" />
<div class="flex items-end gap-3">
<x-media-thumb :url="$slideImageUrl" />
<flux:input wire:model.live.debounce.500ms="slideImageUrl" label="Bild-URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt URL eingeben." class="flex-1" />
</div>
<flux:switch wire:model.live="slideShowBadge" label="Badge anzeigen" description="Kleine Markierung über dem Bild." />
@if($slideShowBadge)
<flux:input wire:model="slideBadge" label="Badge-Text" placeholder="z.B. Einzelstück" />
@endif
</div>
{{-- Texte --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Texte') }}</flux:heading>
<flux:input wire:model="slideTitle" label="Titel" placeholder="z.B. GOYA Sideboard"
description="Hauptüberschrift des Angebots (Zeilenumbruch mit Enter möglich)." />
<flux:switch wire:model.live="slideShowEyebrow" label="Eyebrow anzeigen" description="Kleine Überzeile über dem Titel." />
@if($slideShowEyebrow)
<flux:input wire:model="slideEyebrow" label="Eyebrow" placeholder="z.B. Hersteller: Sudbrock" />
@endif
<flux:switch wire:model.live="slideShowSubline" label="Unterzeile anzeigen" />
@if($slideShowSubline)
<flux:input wire:model="slideSubline" label="Unterzeile" placeholder="z.B. Heute mitnehmen" />
@endif
</div>
{{-- Aufzählung --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Aufzählungspunkte') }}</flux:heading>
<flux:switch wire:model.live="slideShowBullets" label="Aufzählung anzeigen" description="Liste mit Stichpunkten, z.B. Produktdetails." />
@if($slideShowBullets)
<div class="space-y-2">
@foreach($slideBullets as $i => $bullet)
<div class="flex items-center gap-2" wire:key="bullet-{{ $i }}">
<flux:input wire:model="slideBullets.{{ $i }}" placeholder="Punkt {{ $i + 1 }}" class="flex-1" />
<flux:button wire:click="removeBullet({{ $i }})" size="xs" variant="ghost" icon="x-mark" class="text-red-500"></flux:button>
</div>
@endforeach
</div>
<flux:button wire:click="addBullet" size="xs" variant="ghost" icon="plus">
{{ __('Punkt hinzufügen') }}
</flux:button>
@endif
</div>
{{-- Preis --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Preis') }}</flux:heading>
<flux:switch wire:model.live="slideShowPrice" label="Preis anzeigen" />
@if($slideShowPrice)
<flux:input wire:model="slidePrice" label="Preis" placeholder="z.B. 489 €" />
<flux:input wire:model="slideOriginalPrice" label="Originalpreis (optional)" placeholder="z.B. statt 4.744 €" />
@if(trim($slideOriginalPrice) !== '')
<flux:switch wire:model.live="slideStrikeOriginalPrice" label="Streichpreis"
description="Originalpreis wird rot durchgestrichen dargestellt." />
@endif
<flux:input wire:model="slideTagText" label="Hinweis-Tag (optional)" placeholder="z.B. Im Store verfügbar" />
@endif
</div>
{{-- Hinweis --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Hinweis') }}</flux:heading>
<flux:switch wire:model.live="slideShowDisclaimer" label="Disclaimer anzeigen" />
@if($slideShowDisclaimer)
<flux:input wire:model="slideDisclaimer" label="Disclaimer" placeholder="z.B. Zwischenverkauf vorbehalten" />
@endif
</div>
{{-- QR & Kontakt --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('QR-Code & Kontakt') }}</flux:heading>
<flux:switch wire:model.live="slideShowQr" label="QR-Code anzeigen" />
@if($slideShowQr)
<flux:input wire:model="slideQrUrl" label="QR-URL" placeholder="z.B. https://cabinet-bielefeld.de"
description="Leer = es wird die Web/QR-URL aus den Einstellungen genutzt." />
<flux:input wire:model="slideQrTitle" label="QR-Titel" placeholder="z.B. Reservieren" />
@endif
<flux:switch wire:model.live="slideShowContact" label="Kontakt anzeigen" />
@if($slideShowContact)
<flux:input wire:model="slideContact" label="Kontakt" placeholder="z.B. 0521 98620100 / Tel. oder WhatsApp" />
@endif
</div>
{{-- Anzeige --}}
<div class="space-y-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Anzeige') }}</flux:heading>
<flux:input wire:model="slideDuration" type="number" min="1000" step="500" label="Dauer (ms)"
description="Wie lange dieses Angebot eingeblendet wird." />
<flux:switch wire:model="slideIsActive" label="Aktiv" description="Inaktive Angebote werden im Display übersprungen." />
</div>
@endif
<div class="space-y-3 border-t border-zinc-200 pt-6 dark:border-zinc-700">
<div>
<flux:heading size="sm">{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}</flux:heading>
<flux:subheading>{{ __('Zeigt nur den aktuell bearbeiteten Inhalt im Display-Player') }}</flux:subheading>
</div>
{{-- Stable iframe element: only its `src` changes between the
"new" (about:blank) and "saved" state. Swapping the element
structure inside the teleported Flux modal crashes Livewire's
morph/cleanup, so we keep the DOM shape constant. --}}
<div class="relative mx-auto aspect-[9/16] w-full max-w-[320px] overflow-hidden rounded-xl border border-zinc-200 bg-black shadow-sm dark:border-zinc-700">
<iframe
wire:key="item-modal-preview"
src="{{ $itemId ? $this->itemPreviewUrl() : 'about:blank' }}"
class="h-full w-full border-0"
loading="lazy"
title="{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}"
></iframe>
@unless($itemId)
<div class="absolute inset-0 flex items-center justify-center bg-zinc-50 text-center dark:bg-zinc-900">
<div class="px-6 text-sm text-zinc-500 dark:text-zinc-400">
<flux:icon.eye class="mx-auto mb-2 h-8 w-8 opacity-40" />
{{ __('Die Vorschau erscheint, sobald der Inhalt gespeichert wurde.') }}
</div>
</div>
@endunless
</div>
@if($itemId)
<a href="{{ $this->itemPreviewUrl() }}"
target="_blank"
class="mx-auto flex max-w-[320px] items-center justify-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400">
<flux:icon.arrow-top-right-on-square class="w-3 h-3" />
{{ __('Vollbild öffnen') }}
</a>
@endif
</div>
<div class="flex flex-wrap justify-end gap-3 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<flux:button type="submit" variant="primary">
{{ $itemId ? __('Aktualisieren') : __('Hinzufügen') }}
</flux:button>
<flux:button type="button" wire:click="closeItemModal" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="button" wire:click="closeItemModal">
{{ __('Schließen') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
{{-- Settings Modal --}}
<flux:modal :open="$showSettingsModal" wire:model="showSettingsModal">
<form wire:submit.prevent="saveSettings">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Einstellungen') }}</flux:heading>
<flux:subheading>{{ $version->type->label() }}</flux:subheading>
</div>
@include('livewire.admin.cms.partials.version-editor-settings', ['context' => 'modal'])
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="$set('showSettingsModal', false)" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Speichern') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
</div>