- 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>
674 lines
33 KiB
PHP
674 lines
33 KiB
PHP
<?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/<slug></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>
|