b2in/resources/views/livewire/admin/cms/brands-index.blade.php
Kevin Adametz 4b07bbdcae Startseite ruhiger, Otterpohl mit Montage, Marken-Bilder und Farbwelt-Entwurf
- Hinweisleiste per B2IN_ANNOUNCEMENT_BAR ausgeblendet (Standard aus)
- Hero: „Räume aus einer Hand.“ als neue Überschrift, zweiter Button entfernt
- Projekttext mit zentralem deutschen Vertragspartner (Punkt statt Gedankenstrich)
- Otterpohl als vierte Kernmarke: Text v2, Abschnitt „Montage“, Titelbild getauscht
- Markenabschnitte: Absätze mit „- “ als Liste mit fetter Einleitung
- Titelbilder und Galerien für CABINET, Rotpunkt, Febrü und Otterpohl (WebP)
- Feedback 17.09. in den Markentexten, Demobilder entfernt, WhatsApp-Nummer
- Palette „Modern“ fest ohne Schalter, Entwurf /entwurf mit Hero-Slideshow
- Seeder-Liste fürs Deployment im Entwicklungsplan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:34:21 +00:00

674 lines
33 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' => '',
'originCity' => '',
'originDistance' => '',
'originRole' => '',
'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', 'originCity', 'originDistance', 'originRole', '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) ?? '';
$this->originCity = $brand->getTranslation('origin_city', $locale) ?? '';
$this->originDistance = $brand->getTranslation('origin_distance', $locale) ?? '';
$this->originRole = $brand->getTranslation('origin_role', $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,
'originCity' => $this->originCity,
'originDistance' => $this->originDistance,
'originRole' => $this->originRole,
'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',
'originCity' => 'nullable|string|max:255',
'originDistance' => 'nullable|string|max:255',
'originRole' => 'nullable|string|max:500',
'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));
// Herkunfts-Sektion: ohne Fertigungsort erscheint die Marke dort nicht.
$brand->setTranslation('origin_city', $this->editLocale, trim((string) ($validated['originCity'] ?? '')));
$brand->setTranslation('origin_distance', $this->editLocale, trim((string) ($validated['originDistance'] ?? '')));
$brand->setTranslation('origin_role', $this->editLocale, trim((string) ($validated['originRole'] ?? '')));
// 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>
{{-- Fertigungsort für „Aus der Region gefertigt“ (Startseite und /marken) --}}
<div class="mt-6">
<flux:heading size="sm" class="mb-1">Fertigungsort</flux:heading>
<flux:text class="mb-3 text-sm">
Erscheint in der Sektion „Aus der Region gefertigt“ auf der Startseite und unter /marken.
Ohne Ort bleibt die Marke dort weg. Die Position in der Karte ist fest hinterlegt.
</flux:text>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:input wire:model="originCity" label="Ort ({{ strtoupper($editLocale) }})" placeholder="Bünde" />
<flux:input wire:model="originDistance" label="Entfernung von Bielefeld ({{ strtoupper($editLocale) }})"
placeholder="rund 30 km" />
<div class="md:col-span-2">
<flux:input wire:model="originRole" label="Rolle des Standorts ({{ strtoupper($editLocale) }})"
placeholder="Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio." />
</div>
</div>
</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>
<flux:button size="xs" variant="ghost" icon="plus"
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_COMPANY }}')">Werkstatt</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 (erscheint auch unter „Referenzen“, solange dort nichts gepflegt ist).
<strong>Werkstatt</strong> = Einblick in Fertigung oder Montage des Herstellers.
<strong>Inspiration</strong> = Entwurf, Rendering oder KI-Darstellung.
Auf der Seite trägt jedes Bild seine Beschriftung – 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.option value="{{ CmsBrand::GALLERY_COMPANY }}">Aus der Werkstatt</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>