b2in/resources/views/livewire/admin/cms/brands-index.blade.php
Kevin Adametz 4ef3a4d8f1 Markenseiten Rotpunkt und Febrü aus der Zulieferung vom 31.08.
Umsetzung der beiden finalen Webseitentexte aus dev/2026-08-28/.
Die angekündigte Aufgabenliste umsetzung-03-09-2026.md war leer, Grundlage
sind daher die Umsetzungshinweise in den PDFs selbst.

- Neue Felder in cms_brands: sections, widget_label, cta_title, cta_text.
  Einleitung und Leistungskacheln reichten für diese Texte nicht aus.
- Die als Vertiefung gelieferten Blöcke werden aufklappbar dargestellt,
  mit aria-expanded und aria-controls. Bewusst x-transition statt
  x-collapse, weil das Collapse-Plugin hier nicht registriert ist.
- Kein zweiter CTA-Button: das schwebende Terminanfrage-Widget trägt auf
  der Markenseite die Beschriftung der Marke und liest den Kontext aus
  der Route, da es im Layout und nicht je Seite eingebunden ist.
- Footer bekommt unten Platz und lange Beschriftungen brechen um, damit
  das Widget auf dem Smartphone nichts verdeckt.
- Abschnitte sind im Admin vollständig pflegbar, inklusive Sortierung.
- Interne Quellenhinweise der PDFs sind nicht übernommen; ein Test prüft,
  dass sie nicht im Ausgabe-HTML stehen.
- BrandPageTextSeeder fasst nur die Textfelder an, Bilder, Galerie und
  Leistungen bleiben unangetastet.

Vor der Veröffentlichung: Herstellerfreigabe einholen und die Aussage zur
erhaltenen Waldfläche nur mit ausdrücklicher Bestätigung verwenden. Beides
steht in der Deployment-Checkliste.

854 Tests grün (3150 Assertions), Pint sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:52:36 +00:00

635 lines
30 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.

<?php
use App\Models\CmsBrand;
use App\Services\CmsFluxEditorHtmlTransformer;
use Flux\Flux;
use FluxCms\Core\Models\CmsMedia;
use function Livewire\Volt\{computed, layout, on, state, title};
layout('components.layouts.app');
title('Kernmarken verwalten');
state([
'search' => '',
'showForm' => false,
'editingId' => null,
'editLocale' => 'de',
'slug' => '',
'brandName' => '',
'tagline' => '',
'teaser' => '',
'summary' => '',
'highlights' => [],
'description' => '',
'logo' => '',
'image' => '',
'services' => [],
'sections' => [],
'widgetLabel' => '',
'ctaTitle' => '',
'ctaText' => '',
'gallery' => [],
'website_url' => '',
'showroom' => '',
'is_published' => true,
'order' => 0,
]);
on(['media-selected' => function ($mediaId, $url, $field) {
$media = $mediaId ? CmsMedia::find($mediaId) : null;
if (! $media) {
return;
}
if ($field === 'brand_logo') {
$this->logo = $media->filename;
}
if ($field === 'brand_image') {
$this->image = $media->filename;
}
if (str_starts_with($field, 'brand_gallery:')) {
$index = (int) substr($field, strlen('brand_gallery:'));
if (isset($this->gallery[$index])) {
$this->gallery[$index]['image'] = $media->filename;
}
}
}]);
$brands = computed(
fn () => CmsBrand::query()
->when($this->search, fn ($q, $s) => $q->where('slug', 'like', "%{$s}%")
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(name, '$.de')) LIKE ?", ["%{$s}%"]))
->ordered()
->get(),
);
$openCreate = function () {
$this->editingId = null;
$this->reset(['slug', 'brandName', 'tagline', 'teaser', 'summary', 'description', 'logo', 'image', 'website_url', 'showroom', 'widgetLabel', 'ctaTitle', 'ctaText']);
$this->sections = [];
$this->highlights = ['', '', ''];
$this->services = [['title' => '', 'description' => '']];
$this->gallery = [];
$this->is_published = true;
$this->order = (CmsBrand::max('order') ?? 0) + 1;
$this->showForm = true;
};
$loadLocaleFields = function (CmsBrand $brand, string $locale) {
$this->brandName = $brand->getTranslation('name', $locale) ?? '';
$this->tagline = $brand->getTranslation('tagline', $locale) ?? '';
$this->teaser = $brand->getTranslation('teaser', $locale) ?? '';
$this->summary = $brand->getTranslation('summary', $locale) ?? '';
$this->description = CmsFluxEditorHtmlTransformer::toEditor((string) ($brand->getTranslation('description', $locale) ?? ''));
$this->widgetLabel = $brand->getTranslation('widget_label', $locale) ?? '';
$this->ctaTitle = $brand->getTranslation('cta_title', $locale) ?? '';
$this->ctaText = $brand->getTranslation('cta_text', $locale) ?? '';
};
$openEdit = function (int $id) {
$brand = CmsBrand::find($id);
if (! $brand) {
return;
}
$this->editingId = $id;
$this->slug = $brand->slug;
$this->logo = $brand->logo ?? '';
$this->image = $brand->image ?? '';
$this->website_url = $brand->website_url ?? '';
$this->showroom = $brand->showroom ?? '';
$this->is_published = $brand->is_published;
$this->order = $brand->order ?? 0;
$this->highlights = $brand->highlights ?: ['', '', ''];
$this->services = $brand->services ?: [['title' => '', 'description' => '']];
$this->gallery = CmsBrand::normalizeGallery($brand->gallery);
// Absätze im Formular als Textblock: eine Leerzeile trennt sie.
$this->sections = collect(CmsBrand::normalizeSections($brand->sections))
->map(fn (array $s) => [
'title' => $s['title'],
'paragraphs' => implode("\n\n", $s['paragraphs']),
'claim' => $s['claim'],
'quote' => $s['quote'],
'quote_author' => $s['quote_author'],
'detail_title' => $s['detail']['title'] ?? '',
'detail_paragraphs' => implode("\n\n", $s['detail']['paragraphs'] ?? []),
])
->all();
$this->loadLocaleFields($brand, $this->editLocale);
$this->showForm = true;
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId) {
$brand = CmsBrand::find($this->editingId);
if ($brand) {
$this->loadLocaleFields($brand, $locale);
}
}
};
$addHighlight = fn () => $this->highlights[] = '';
$removeHighlight = function (int $index) {
unset($this->highlights[$index]);
$this->highlights = array_values($this->highlights);
};
$addService = fn () => $this->services[] = ['title' => '', 'description' => ''];
$removeService = function (int $index) {
unset($this->services[$index]);
$this->services = array_values($this->services);
};
$addSection = fn () => $this->sections[] = [
'title' => '', 'paragraphs' => '', 'claim' => '',
'quote' => '', 'quote_author' => '', 'detail_title' => '', 'detail_paragraphs' => '',
];
$removeSection = function (int $index) {
unset($this->sections[$index]);
$this->sections = array_values($this->sections);
};
$moveSection = function (int $index, int $direction) {
$target = $index + $direction;
if (! isset($this->sections[$index], $this->sections[$target])) {
return;
}
[$this->sections[$index], $this->sections[$target]] = [$this->sections[$target], $this->sections[$index]];
};
$addGalleryImage = function (string $category = CmsBrand::GALLERY_PROJECT) {
$this->gallery[] = [
'image' => '',
'category' => in_array($category, CmsBrand::GALLERY_CATEGORIES, true) ? $category : CmsBrand::GALLERY_INSPIRATION,
'caption' => '',
];
};
$removeGalleryImage = function (int $index) {
unset($this->gallery[$index]);
$this->gallery = array_values($this->gallery);
};
$save = function () {
$validated = validator([
'slug' => $this->slug,
'brandName' => $this->brandName,
'tagline' => $this->tagline,
'teaser' => $this->teaser,
'summary' => $this->summary,
'website_url' => $this->website_url,
'showroom' => $this->showroom,
'is_published' => $this->is_published,
'order' => $this->order,
], [
'slug' => 'required|string|max:255|regex:/^[a-z0-9-]+$/',
'brandName' => 'required|string|max:255',
'tagline' => 'nullable|string|max:255',
'teaser' => 'nullable|string|max:2000',
'summary' => 'nullable|string|max:4000',
'website_url' => 'nullable|url|max:500',
'showroom' => 'nullable|string|max:255',
'is_published' => 'boolean',
'order' => 'integer|min:0',
], [
'slug.regex' => 'Der Slug darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten.',
'website_url.url' => 'Bitte eine vollständige URL angeben (mit https://).',
])->validate();
$duplicate = CmsBrand::query()
->where('slug', $validated['slug'])
->when($this->editingId, fn ($q) => $q->whereKeyNot($this->editingId))
->exists();
if ($duplicate) {
Flux::toast(variant: 'danger', heading: 'Slug vergeben', text: 'Es gibt bereits eine Marke mit diesem Slug.');
return;
}
$brand = $this->editingId
? CmsBrand::findOrFail($this->editingId)
: CmsBrand::query()->make();
$brand->slug = $validated['slug'];
$brand->setTranslation('name', $this->editLocale, $validated['brandName']);
$brand->setTranslation('tagline', $this->editLocale, $validated['tagline'] ?? '');
$brand->setTranslation('teaser', $this->editLocale, $validated['teaser'] ?? '');
$brand->setTranslation('summary', $this->editLocale, $validated['summary'] ?? '');
$brand->setTranslation('description', $this->editLocale, CmsFluxEditorHtmlTransformer::fromEditor((string) $this->description));
$brand->logo = $this->logo ?: null;
$brand->image = $this->image ?: null;
$brand->website_url = $validated['website_url'] ?: null;
$brand->showroom = $validated['showroom'] ?: null;
$brand->highlights = collect($this->highlights)
->map(fn ($h) => trim((string) $h))
->filter()
->values()
->toArray();
$brand->services = collect($this->services)
->filter(fn ($s) => ! empty($s['title']) || ! empty($s['description']))
->values()
->toArray();
$brand->gallery = CmsBrand::normalizeGallery($this->gallery);
$brand->setTranslation('widget_label', $this->editLocale, trim((string) $this->widgetLabel));
$brand->setTranslation('cta_title', $this->editLocale, trim((string) $this->ctaTitle));
$brand->setTranslation('cta_text', $this->editLocale, trim((string) $this->ctaText));
// normalizeSections() nimmt die Textblöcke entgegen und trennt sie an den Leerzeilen.
$brand->sections = CmsBrand::normalizeSections(collect($this->sections)->map(fn (array $s) => [
'title' => $s['title'] ?? '',
'paragraphs' => $s['paragraphs'] ?? '',
'claim' => $s['claim'] ?? '',
'quote' => $s['quote'] ?? '',
'quote_author' => $s['quote_author'] ?? '',
'detail' => [
'title' => $s['detail_title'] ?? '',
'paragraphs' => $s['detail_paragraphs'] ?? '',
],
])->all());
$brand->is_published = $validated['is_published'];
$brand->order = $validated['order'];
$brand->save();
$this->showForm = false;
$this->editingId = null;
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Marke wurde erfolgreich gespeichert.');
};
$togglePublished = function (int $id) {
$brand = CmsBrand::findOrFail($id);
$brand->update(['is_published' => ! $brand->is_published]);
Flux::toast(heading: 'Status geändert', text: $brand->is_published ? 'Veröffentlicht' : 'Entwurf');
};
$deleteBrand = function (int $id) {
CmsBrand::findOrFail($id)->delete();
Flux::toast(variant: 'success', heading: 'Gelöscht', text: 'Marke wurde entfernt.');
};
$cancelForm = function () {
$this->showForm = false;
$this->editingId = null;
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Kernmarken</flux:heading>
<div class="flex items-center gap-2">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
<flux:button variant="primary" icon="plus" wire:click="openCreate">Neue Marke</flux:button>
</div>
</div>
<flux:card class="mb-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-3">
<flux:icon.information-circle class="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" />
<div class="text-sm text-blue-800 dark:text-blue-200">
Die Marken erscheinen als Kacheln auf der Startseite unter „Unsere Kernmarken" und
bekommen jeweils eine eigene Detailseite unter <code>/marken/&lt;slug&gt;</code>.
Die Reihenfolge steuert das Feld „Sortierung".
</div>
</div>
</flux:card>
<div class="mb-4">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Marke suchen..." icon="magnifying-glass"
size="sm" class="w-64" />
</div>
@if ($showForm)
<flux:card class="mb-6">
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ $editingId ? 'Marke bearbeiten' : 'Neue Marke' }}</flux:heading>
<div class="flex gap-1">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:input wire:model="slug" label="Slug (Teil der Adresse)" placeholder="cabinet" />
@error('slug') <span class="text-sm text-red-600">{{ $message }}</span> @enderror
<flux:input wire:model="brandName" label="Name ({{ strtoupper($editLocale) }})" placeholder="CABINET" />
@error('brandName') <span class="text-sm text-red-600">{{ $message }}</span> @enderror
<div class="md:col-span-2">
<flux:input wire:model="tagline" label="Claim ({{ strtoupper($editLocale) }})"
placeholder="Einbauschränke nach Maß." />
</div>
<flux:input wire:model="showroom" label="Showroom (optional)" placeholder="Showroom Bielefeld" />
<flux:input wire:model="website_url" label="Herstellerseite (optional)" placeholder="https://..." />
@error('website_url') <span class="text-sm text-red-600">{{ $message }}</span> @enderror
<flux:input wire:model="order" label="Sortierung" type="number" />
</div>
<div class="mt-4 space-y-4">
<flux:textarea wire:model="teaser" label="Kacheltext Startseite ({{ strtoupper($editLocale) }})" rows="3"
placeholder="Kurztext für die Kachel auf der Startseite..." />
<flux:textarea wire:model="summary" label="Text auf /marken ({{ strtoupper($editLocale) }})" rows="6"
placeholder="Ausführlicher Text für die Markenübersicht. Leerzeile trennt Absätze." />
</div>
{{-- Drei Merkmale für die Übersichtsseite --}}
<div class="mt-6">
<div class="mb-2 flex items-center justify-between">
<flux:heading size="sm">Merkmale auf /marken</flux:heading>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addHighlight">Merkmal hinzufügen</flux:button>
</div>
<div class="space-y-2">
@foreach ($highlights as $i => $highlight)
<div wire:key="highlight-{{ $i }}" class="flex items-center gap-2">
<flux:input wire:model="highlights.{{ $i }}" class="flex-1"
placeholder="z.B. Millimetergenaue Planung" />
<flux:button size="xs" variant="ghost" icon="trash" wire:click="removeHighlight({{ $i }})" />
</div>
@endforeach
</div>
</div>
{{-- Bilder --}}
<div class="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Logo</label>
<div class="flex items-start gap-3">
@if ($logo)
<div class="flex h-16 w-24 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-zinc-200 bg-white p-2 dark:border-zinc-700">
<img src="{{ media_url($logo) }}" class="max-h-full max-w-full object-contain" />
</div>
@endif
<div class="flex-1">
<livewire:admin.cms.media-picker :value="null" field="brand_logo" type="image"
label="Logo wählen" :key="'brand-logo-' . ($editingId ?? 'new')" />
<p class="mt-1 text-xs text-zinc-400">{{ $logo ?: 'Kein Logo' }}</p>
</div>
</div>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Stimmungsbild</label>
<div class="flex items-start gap-3">
@if ($image)
<div class="h-16 w-24 shrink-0 overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700">
<img src="{{ media_url($image) }}" class="h-full w-full object-cover" />
</div>
@endif
<div class="flex-1">
<livewire:admin.cms.media-picker :value="null" field="brand_image" type="image"
profile="card" label="Bild wählen" :key="'brand-img-' . ($editingId ?? 'new')" />
<p class="mt-1 text-xs text-zinc-400">{{ $image ?: 'Kein Bild' }}</p>
</div>
</div>
</div>
</div>
{{-- Einleitung --}}
<div class="mt-6">
<flux:heading size="sm" class="mb-3">Einleitung ({{ strtoupper($editLocale) }})</flux:heading>
<flux:editor wire:model="description" />
</div>
{{-- Leistungen --}}
<div class="mt-6">
<div class="mb-2 flex items-center justify-between">
<flux:heading size="sm">Leistungen und Besonderheiten</flux:heading>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addService">Punkt hinzufügen</flux:button>
</div>
<div class="space-y-4">
@foreach ($services as $i => $service)
<div wire:key="service-{{ $i }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-medium text-zinc-500">Punkt {{ $i + 1 }}</span>
@if (count($services) > 1)
<flux:button size="xs" variant="ghost" icon="trash" wire:click="removeService({{ $i }})" />
@endif
</div>
<flux:input wire:model="services.{{ $i }}.title" label="Überschrift" class="mb-3"
placeholder="z.B. Aufmaß vor Ort" />
<flux:textarea wire:model="services.{{ $i }}.description" label="Text" rows="2"
placeholder="Kurze Erläuterung..." />
</div>
@endforeach
</div>
</div>
{{-- Abschnitte der Markenseite --}}
<div class="mt-6">
<div class="mb-2 flex items-center justify-between">
<flux:heading size="sm">Abschnitte der Markenseite</flux:heading>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addSection">Abschnitt hinzufügen</flux:button>
</div>
<div class="mb-3 rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800/50 dark:text-zinc-300">
Die Abschnitte erscheinen auf der Detailseite unter der Einleitung, in dieser Reihenfolge.
<strong>Eine Leerzeile</strong> trennt zwei Absätze. Die <strong>Vertiefung</strong> wird auf
der Seite als aufklappbarer Bereich dargestellt – leer lassen, wenn es keine gibt.
</div>
@if (empty($sections))
<p class="text-sm text-zinc-400">Noch keine Abschnitte.</p>
@else
<div class="space-y-4">
@foreach ($sections as $i => $section)
<div wire:key="section-{{ $i }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-3 flex items-center justify-between">
<span class="text-xs font-medium text-zinc-500">Abschnitt {{ $i + 1 }}</span>
<div class="flex items-center gap-1">
<flux:button size="xs" variant="ghost" icon="arrow-up"
wire:click="moveSection({{ $i }}, -1)" :disabled="$i === 0" />
<flux:button size="xs" variant="ghost" icon="arrow-down"
wire:click="moveSection({{ $i }}, 1)" :disabled="$i === count($sections) - 1" />
<flux:button size="xs" variant="ghost" icon="trash" wire:click="removeSection({{ $i }})" />
</div>
</div>
<flux:input wire:model="sections.{{ $i }}.title" label="Überschrift" class="mb-3"
placeholder="z.B. Aus Ostwestfalen. Für Ihr Zuhause." />
<flux:textarea wire:model="sections.{{ $i }}.paragraphs" label="Text" rows="5" class="mb-3"
placeholder="Absätze durch eine Leerzeile trennen..." />
<flux:input wire:model="sections.{{ $i }}.claim" label="Schlusszeile (optional)" class="mb-3"
placeholder="Wird hervorgehoben unter den Text gesetzt" />
<div class="mb-3 grid grid-cols-1 gap-3 md:grid-cols-3">
<div class="md:col-span-2">
<flux:textarea wire:model="sections.{{ $i }}.quote" label="Zitat (optional)" rows="2" />
</div>
<flux:input wire:model="sections.{{ $i }}.quote_author" label="Zitat von" placeholder="Name, Funktion" />
</div>
<div class="rounded-md border border-dashed border-zinc-300 p-3 dark:border-zinc-600">
<p class="mb-2 text-xs font-medium text-zinc-500">Vertiefung – erscheint aufklappbar</p>
<flux:input wire:model="sections.{{ $i }}.detail_title" label="Überschrift der Vertiefung" class="mb-3" />
<flux:textarea wire:model="sections.{{ $i }}.detail_paragraphs" label="Text der Vertiefung" rows="4"
placeholder="Absätze durch eine Leerzeile trennen..." />
</div>
</div>
@endforeach
</div>
@endif
</div>
{{-- Abschlussblock und Beschriftung des Terminanfrage-Widgets --}}
<div class="mt-6 space-y-4">
<flux:heading size="sm">Abschluss und Terminanfrage</flux:heading>
<flux:input wire:model="widgetLabel" label="Beschriftung des Terminanfrage-Widgets ({{ strtoupper($editLocale) }})"
placeholder="z.B. Persönliche Küchenberatung vereinbaren" />
<p class="-mt-3 text-xs text-zinc-400">
Steht auf dem schwebenden Button, solange man auf dieser Markenseite ist. Leer lassen für die
allgemeine Beschriftung.
</p>
<flux:input wire:model="ctaTitle" label="Überschrift Abschlussblock ({{ strtoupper($editLocale) }})"
placeholder="z.B. Ihre Küche. Persönlich geplant." />
<flux:textarea wire:model="ctaText" label="Text Abschlussblock ({{ strtoupper($editLocale) }})" rows="3" />
</div>
{{-- Galerie --}}
<div class="mt-6">
<div class="mb-2 flex items-center justify-between">
<flux:heading size="sm">Bildergalerie</flux:heading>
<div class="flex gap-1">
<flux:button size="xs" variant="ghost" icon="plus"
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_PROJECT }}')">Projekt</flux:button>
<flux:button size="xs" variant="ghost" icon="plus"
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_INSPIRATION }}')">Inspiration</flux:button>
</div>
</div>
<div class="mb-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
<strong>Projekt</strong> = tatsächlich ausgeführter Auftrag.
<strong>Inspiration</strong> = Entwurf, Rendering oder KI-Darstellung.
Auf der Seite werden beide getrennt gezeigt und beschriftet – bitte nichts vermischen.
</div>
@if (empty($gallery))
<p class="text-sm text-zinc-400">Noch keine Bilder.</p>
@else
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
@foreach ($gallery as $i => $item)
<div wire:key="gallery-{{ $i }}" class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
@if (!empty($item['image']))
<div class="mb-2 h-24 w-full overflow-hidden rounded-md">
<img src="{{ media_url($item['image']) }}" class="h-full w-full object-cover" />
</div>
@endif
<flux:select wire:model="gallery.{{ $i }}.category" size="sm" class="mb-2">
<flux:select.option value="{{ CmsBrand::GALLERY_PROJECT }}">Realisiertes Projekt</flux:select.option>
<flux:select.option value="{{ CmsBrand::GALLERY_INSPIRATION }}">Inspiration / Entwurf</flux:select.option>
</flux:select>
<flux:input wire:model="gallery.{{ $i }}.caption" size="sm" class="mb-2"
placeholder="Bildunterschrift (optional)" />
<div class="flex items-center justify-between gap-2">
<livewire:admin.cms.media-picker :value="null" field="brand_gallery:{{ $i }}"
type="image" profile="card" label="Wählen"
:key="'brand-gallery-' . ($editingId ?? 'new') . '-' . $i" />
<flux:button size="xs" variant="ghost" icon="trash" wire:click="removeGalleryImage({{ $i }})" />
</div>
<flux:input wire:model="gallery.{{ $i }}.image" size="sm" class="mt-2"
placeholder="Oder Dateiname/URL manuell..." />
</div>
@endforeach
</div>
@endif
</div>
<div class="mt-4">
<flux:switch wire:model="is_published" label="Veröffentlicht" />
</div>
<div class="mt-4 flex gap-2">
<flux:button variant="primary" wire:click="save">Speichern</flux:button>
<flux:button variant="ghost" wire:click="cancelForm">Abbrechen</flux:button>
</div>
</flux:card>
@endif
<flux:card>
<div class="divide-y dark:divide-zinc-700">
@forelse ($this->brands as $brand)
<div wire:key="brand-{{ $brand->id }}" class="flex items-center justify-between gap-4 py-3">
<div class="flex min-w-0 flex-1 items-center gap-3">
@if ($brand->image)
<div class="h-12 w-20 shrink-0 overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-700">
<img src="{{ media_url($brand->image) }}" alt="" class="h-full w-full object-cover" loading="lazy" />
</div>
@else
<div class="flex h-12 w-20 shrink-0 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-700">
<flux:icon.building-storefront class="h-6 w-6 text-zinc-400" />
</div>
@endif
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="truncate font-medium text-zinc-800 dark:text-zinc-200">
{{ $brand->getTranslation('name', $editLocale) }}
</span>
@unless ($brand->is_published)
<flux:badge size="sm" color="amber">Entwurf</flux:badge>
@endunless
</div>
<p class="truncate text-sm text-zinc-500">
{{ $brand->getTranslation('tagline', $editLocale) ?: '–' }} · /marken/{{ $brand->slug }}
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-1">
<flux:button size="sm" variant="ghost" icon="arrow-top-right-on-square"
href="{{ url('/marken/'.$brand->slug) }}" target="_blank" />
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="openEdit({{ $brand->id }})" />
<flux:button size="sm" variant="ghost" :icon="$brand->is_published ? 'eye' : 'eye-slash'"
wire:click="togglePublished({{ $brand->id }})" />
<flux:button size="sm" variant="ghost" icon="trash" wire:click="deleteBrand({{ $brand->id }})"
wire:confirm="Marke wirklich löschen?" />
</div>
</div>
@empty
<div class="py-12 text-center">
<flux:icon.building-storefront class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
<flux:heading>Keine Marken</flux:heading>
<flux:text>Lege die erste Kernmarke mit dem Button oben an.</flux:text>
</div>
@endforelse
</div>
</flux:card>
</div>