b2in/resources/views/livewire/admin/cms/content-index.blade.php
2026-04-10 17:18:17 +02:00

782 lines
38 KiB
PHP

<?php
use App\Services\CmsFluxEditorHtmlTransformer;
use Flux\Flux;
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\CmsContentService;
use FluxCms\Core\Services\HeroiconOutlineList;
use function Livewire\Volt\{layout, title, state, computed, on};
if (! function_exists('_cmsParseJsonItems')) {
function _cmsParseJsonItems(array $value): array
{
if (empty($value)) {
return [false, []];
}
$isList = array_is_list($value);
$stringify = fn ($v) => is_array($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string) $v;
if ($isList && ! is_array($value[0])) {
return [true, array_map(fn ($v) => ['_value' => $stringify($v)], $value)];
}
if ($isList) {
return [false, array_map(fn ($item) => is_array($item)
? array_map($stringify, $item)
: ['_value' => (string) $item], $value)];
}
return [false, [array_map($stringify, $value)]];
}
}
if (! function_exists('_cmsFieldLooksLikeImage')) {
function _cmsFieldLooksLikeImage(string $fieldKey, mixed $fieldValue): bool
{
if (! is_string($fieldValue) || trim($fieldValue) === '') {
return false;
}
$key = strtolower($fieldKey);
if (in_array($key, ['image', 'photo', 'avatar', 'picture', 'thumbnail', 'img', 'hero_image', 'background_image', 'cover_image'], true)) {
return true;
}
if (preg_match('/_(image|photo|avatar|picture|thumb)$/i', $fieldKey)) {
return true;
}
return (bool) preg_match('/\.(jpe?g|png|gif|webp|svg)$/i', $fieldValue);
}
}
layout('components.layouts.app');
title('CMS Inhalte');
state([
'selectedGroup' => null,
'search' => '',
'editingId' => null,
'editingField' => null,
'editLocale' => 'de',
'editValue' => '',
'editMediaId' => null,
'showJsonModal' => false,
'jsonItems' => [],
'jsonIsStringArray' => false,
'jsonEditingKey' => '',
'editingFieldType' => 'text',
]);
on(['media-selected' => function (string $field, ?int $mediaId, ?string $url) {
if ($field === 'content_image') {
$media = $mediaId ? CmsMedia::find($mediaId) : null;
if ($media) {
$this->editValue = $media->filename;
$this->editMediaId = $mediaId;
} else {
$this->editValue = '';
$this->editMediaId = null;
}
return;
}
if (str_starts_with($field, 'jsonimg:')) {
$parts = explode(':', $field, 3);
if (count($parts) === 3 && $mediaId) {
$media = CmsMedia::find($mediaId);
if ($media) {
$idx = (int) $parts[1];
$fname = $parts[2];
if (isset($this->jsonItems[$idx]) && is_array($this->jsonItems[$idx])) {
$this->jsonItems[$idx][$fname] = $media->filename;
}
}
}
return;
}
}]);
$groups = computed(fn () => CmsContent::query()
->selectRaw('`group`, count(*) as count')
->groupBy('group')
->orderBy('group')
->pluck('count', 'group')
->toArray());
$flatContents = computed(function () {
if (! $this->selectedGroup) {
return collect();
}
$contents = CmsContent::forGroup($this->selectedGroup)
->orderBy('order')
->get();
$rows = [];
foreach ($contents as $content) {
$value = $content->getTranslation('value', $this->editLocale);
if ($content->type === 'json' && is_array($value) && ! array_is_list($value)) {
foreach ($value as $fieldKey => $fieldValue) {
if ($this->search && ! str_contains(strtolower($content->key . '.' . $fieldKey), strtolower($this->search))) {
continue;
}
$fieldType = 'text';
if ($this->selectedGroup === 'legal' && $fieldKey === 'content') {
$fieldType = 'legal_html';
} elseif (is_array($fieldValue)) {
$fieldType = 'json';
} elseif (is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue)) {
$fieldType = 'html';
} elseif (_cmsFieldLooksLikeImage($fieldKey, $fieldValue)) {
$fieldType = 'image';
}
$rows[] = (object) [
'content_id' => $content->id,
'section_key' => $content->key,
'field_key' => $fieldKey,
'display_key' => $content->key . '.' . $fieldKey,
'type' => $fieldType,
'value' => $fieldValue,
'is_subfield' => true,
];
}
} else {
if ($this->search && ! str_contains(strtolower($content->key), strtolower($this->search))) {
continue;
}
$rows[] = (object) [
'content_id' => $content->id,
'section_key' => $content->key,
'field_key' => null,
'display_key' => $content->key,
'type' => $content->type ?? 'text',
'value' => $value,
'is_subfield' => false,
];
}
}
return collect($rows);
});
$availableIcons = computed(fn () => HeroiconOutlineList::names());
$selectGroup = function (string $group) {
$this->showJsonModal = false;
$this->jsonItems = [];
$this->jsonIsStringArray = false;
$this->jsonEditingKey = '';
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editMediaId = null;
$this->editingFieldType = 'text';
$this->selectedGroup = $group;
};
$startFieldEdit = function (int $contentId, ?string $fieldKey = null) {
$content = CmsContent::find($contentId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $this->editLocale);
if ($fieldKey !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$fieldKey] ?? '';
if (is_array($fieldValue)) {
$this->editingId = $contentId;
$this->editingField = $fieldKey;
$this->editingFieldType = 'text';
$this->jsonEditingKey = $content->key . '.' . $fieldKey;
$isList = array_is_list($fieldValue);
if ($isList) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fieldValue);
} else {
$this->jsonIsStringArray = false;
$this->jsonItems = [array_map(
fn ($v) => is_array($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string) $v,
$fieldValue,
)];
}
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
$this->showJsonModal = true;
return;
}
$this->editingId = $contentId;
$this->editingField = $fieldKey;
$this->editValue = (string) $fieldValue;
if (_cmsFieldLooksLikeImage($fieldKey, $fieldValue)) {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->group === 'legal' && $fieldKey === 'content') {
$this->editingFieldType = 'legal_html';
$this->editMediaId = null;
} else {
$this->editingFieldType = is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue) ? 'html' : 'text';
$this->editMediaId = null;
}
if ($this->editingFieldType === 'html') {
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
}
} else {
$this->editingId = $contentId;
$this->editingField = null;
if ($content->type === 'json') {
$this->jsonEditingKey = $content->key;
if (is_array($fullValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fullValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
} else {
$this->jsonIsStringArray = false;
$this->jsonItems = [];
}
$this->showJsonModal = true;
return;
}
$this->editValue = is_array($fullValue)
? json_encode($fullValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: ((string) ($fullValue ?? ''));
if ($content->type === 'image') {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->type === 'html') {
$this->editingFieldType = 'html';
$this->editMediaId = null;
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
} else {
$this->editingFieldType = 'text';
$this->editMediaId = null;
}
}
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId && $this->showJsonModal) {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $locale);
if ($this->editingField !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$this->editingField] ?? [];
if (is_array($fieldValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fieldValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
}
} elseif (is_array($fullValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fullValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
}
} elseif ($this->editingId) {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $locale);
if ($this->editingField !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$this->editingField] ?? '';
$this->editValue = is_array($fieldValue)
? json_encode($fieldValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: (string) $fieldValue;
if (! is_array($fieldValue)) {
if (_cmsFieldLooksLikeImage((string) $this->editingField, $fieldValue)) {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->group === 'legal' && $this->editingField === 'content') {
$this->editingFieldType = 'legal_html';
$this->editMediaId = null;
} else {
$this->editingFieldType = is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue) ? 'html' : 'text';
$this->editMediaId = null;
}
if ($this->editingFieldType === 'html') {
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
}
}
} else {
$this->editValue = is_array($fullValue)
? json_encode($fullValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: (string) ($fullValue ?? '');
if ($content->type === 'image') {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->type === 'html') {
$this->editingFieldType = 'html';
$this->editMediaId = null;
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
} else {
$this->editingFieldType = 'text';
$this->editMediaId = null;
}
}
}
};
$saveEdit = function () {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$valueToSave = $this->editValue;
if ($this->editingFieldType === 'html') {
$valueToSave = CmsFluxEditorHtmlTransformer::fromEditor($valueToSave);
}
if ($this->editingField !== null) {
$fullValue = $content->getTranslation('value', $this->editLocale);
if (! is_array($fullValue)) {
$fullValue = [];
}
$fullValue[$this->editingField] = $valueToSave;
$content->setTranslation('value', $this->editLocale, $fullValue);
} else {
$content->setTranslation('value', $this->editLocale, $valueToSave);
}
$content->save();
app(CmsContentService::class)->clearCache($this->selectedGroup);
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editingFieldType = 'text';
$this->editMediaId = null;
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.');
};
$addJsonItem = function () {
if ($this->jsonIsStringArray) {
$this->jsonItems[] = ['_value' => ''];
} elseif (! empty($this->jsonItems)) {
$template = array_map(fn () => '', $this->jsonItems[0]);
$this->jsonItems[] = $template;
}
};
$removeJsonItem = function (int $index) {
unset($this->jsonItems[$index]);
$this->jsonItems = array_values($this->jsonItems);
};
$saveJsonModal = function () {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$itemsForSave = CmsFluxEditorHtmlTransformer::fromEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
if ($this->jsonIsStringArray) {
$newValue = array_values(array_map(fn ($item) => $item['_value'] ?? '', $itemsForSave));
} else {
$newValue = array_values(
array_map(function ($item) {
$cleaned = [];
foreach ($item as $k => $v) {
if (is_string($v) && (str_starts_with($v, '[') || str_starts_with($v, '{'))) {
$decoded = json_decode($v, true);
$cleaned[$k] = json_last_error() === JSON_ERROR_NONE ? $decoded : $v;
} else {
$cleaned[$k] = $v;
}
}
return $cleaned;
}, $itemsForSave),
);
}
if ($this->editingField !== null) {
$fullValue = $content->getTranslation('value', $this->editLocale);
if (! is_array($fullValue)) {
$fullValue = [];
}
$fullValue[$this->editingField] = $newValue;
$content->setTranslation('value', $this->editLocale, $fullValue);
} else {
$content->setTranslation('value', $this->editLocale, $newValue);
}
$content->save();
app(CmsContentService::class)->clearCache($this->selectedGroup);
$this->showJsonModal = false;
$this->editingId = null;
$this->editingField = null;
$this->jsonItems = [];
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.');
};
$cancelEdit = function () {
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editingFieldType = 'text';
$this->editMediaId = null;
};
$cancelJsonModal = function () {
$this->showJsonModal = false;
$this->editingId = null;
$this->editingField = null;
$this->jsonItems = [];
};
?>
@php
$cmsGroupLabels = [
'legal' => 'Rechtliches',
];
@endphp
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Inhalte verwalten</flux:heading>
<div class="flex items-center gap-2">
<flux:badge color="blue">{{ array_sum($this->groups) }} Einträge</flux:badge>
</div>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-4">
<div class="lg:col-span-1">
<flux:card>
<flux:heading size="sm" class="mb-3">Seiten / Gruppen</flux:heading>
<div class="flex flex-col gap-1">
@foreach ($this->groups as $group => $count)
<button wire:click="selectGroup('{{ $group }}')"
class="flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-colors {{ $selectedGroup === $group ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'hover:bg-zinc-100 dark:hover:bg-zinc-700' }}">
<span>{{ $cmsGroupLabels[$group] ?? $group }}</span>
<flux:badge size="sm">{{ $count }}</flux:badge>
</button>
@endforeach
</div>
</flux:card>
</div>
<div class="lg:col-span-3" wire:key="cms-content-panel-{{ $selectedGroup ?? 'empty' }}">
@if ($selectedGroup)
<flux:card>
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ $cmsGroupLabels[$selectedGroup] ?? $selectedGroup }}</flux:heading>
<div class="flex items-center gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen..." size="sm"
icon="magnifying-glass" class="w-48" />
<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>
@php $lastSection = null; @endphp
<div class="divide-y dark:divide-zinc-700">
@forelse ($this->flatContents as $row)
@if ($row->section_key !== $lastSection)
@php $lastSection = $row->section_key; @endphp
<div class="bg-zinc-50 dark:bg-zinc-800/50 px-3 py-2">
<span class="text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">{{ $row->section_key }}</span>
</div>
@endif
@php
$isEditing = $editingId === $row->content_id
&& $editingField === $row->field_key
&& ! $showJsonModal;
$editKey = $row->field_key !== null
? $row->content_id . ",'" . $row->field_key . "'"
: $row->content_id . ',null';
@endphp
<div wire:key="row-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}" class="py-3 px-1">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center gap-2">
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400">{{ $row->field_key ?? $row->section_key }}</code>
@php
$typeColors = ['html' => 'amber', 'legal_html' => 'blue', 'image' => 'green', 'json' => 'violet', 'link' => 'rose'];
$badgeColor = $typeColors[$row->type] ?? 'zinc';
@endphp
<flux:badge size="sm" :color="$badgeColor">{{ $row->type }}</flux:badge>
</div>
<div
wire:key="field-value-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}-{{ $isEditing ? 'edit' : 'view' }}"
>
@if ($isEditing)
<div class="mt-2">
@if ($editingFieldType === 'image')
<div class="flex flex-col gap-3 sm:flex-row sm:items-start">
@if ($editValue)
<div class="h-24 w-36 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($editValue) }}" alt="" class="h-full w-full object-cover" />
</div>
@endif
<div class="min-w-0 flex-1 space-y-2">
<livewire:admin.cms.media-picker
:value="$editMediaId"
field="content_image"
type="image"
profile="card"
label="Bild aus Medienbibliothek wählen"
:key="'cms-content-img-' . $selectedGroup . '-' . $editingId . '-' . ($editingField ?? 'root') . '-' . $editLocale"
/>
<flux:input wire:model="editValue" size="sm" label="Dateiname (optional manuell)" placeholder="z. B. b2in/hero.jpg" />
</div>
</div>
@elseif ($editingFieldType === 'html')
<flux:editor wire:model="editValue" toolbar="bold italic highlight"
class="**:data-[slot=content]:min-h-[60px]!" />
@elseif ($editingFieldType === 'legal_html')
{{-- natives textarea, Inhalt nur per Livewire-JS (kein </textarea> im HTML); verhindert DOM-Bruch bei Impressum-HTML --}}
<div class="space-y-2">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300" for="legal-html-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}">HTML (Rohtext)</label>
<textarea
id="legal-html-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}"
wire:model.defer="editValue"
rows="28"
spellcheck="false"
class="block min-h-[24rem] w-full resize-y rounded-lg border border-zinc-200 bg-white p-3 font-mono text-xs text-zinc-800 shadow-xs dark:border-white/10 dark:bg-white/10 dark:text-zinc-200"
></textarea>
</div>
@else
<flux:textarea wire:model="editValue" rows="8" class="font-mono text-sm" />
@endif
<div class="mt-2 flex gap-2">
<flux:button size="sm" variant="primary" wire:click="saveEdit" wire:loading.attr="disabled" wire:target="saveEdit">
<span wire:loading.remove wire:target="saveEdit">Speichern</span>
<span wire:loading wire:target="saveEdit">Speichern…</span>
</flux:button>
<flux:button size="sm" variant="ghost" wire:click="cancelEdit" wire:loading.attr="disabled" wire:target="saveEdit">Abbrechen</flux:button>
</div>
</div>
@else
@if ($row->type === 'json' && is_array($row->value))
@php
$isList = array_is_list($row->value);
$cnt = count($row->value);
@endphp
<div class="flex items-center gap-2 text-sm text-zinc-500">
<flux:badge size="sm" color="violet">{{ $cnt }} {{ $isList ? 'Einträge' : 'Felder' }}</flux:badge>
@if ($isList && $cnt > 0 && is_array($row->value[0] ?? null))
<span class="text-xs text-zinc-400">{{ implode(', ', array_keys($row->value[0])) }}</span>
@elseif (! $isList)
<span class="text-xs text-zinc-400">{{ implode(', ', array_slice(array_keys($row->value), 0, 5)) }}{{ $cnt > 5 ? ', …' : '' }}</span>
@endif
</div>
@elseif ($row->type === 'image' && is_string($row->value) && $row->value !== '')
<div class="flex items-center gap-3">
<div class="h-14 w-24 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($row->value) }}" alt="" class="h-full w-full object-cover" loading="lazy" />
</div>
<span class="truncate text-xs text-zinc-500">{{ $row->value }}</span>
</div>
@elseif ($row->type === 'legal_html')
<p class="line-clamp-3 text-sm text-zinc-600 dark:text-zinc-400">
{{ \Illuminate\Support\Str::limit(strip_tags((string) $row->value), 200) }}
</p>
@elseif ($row->type === 'html')
<div class="prose prose-sm max-w-none text-zinc-800 dark:prose-invert dark:text-zinc-200 line-clamp-2">
{!! \Illuminate\Support\Str::limit((string) $row->value, 200) !!}
</div>
@else
<p class="truncate text-sm text-zinc-800 dark:text-zinc-200">
{{ \Illuminate\Support\Str::limit(strip_tags(is_array($row->value) ? json_encode($row->value, JSON_UNESCAPED_UNICODE) : (string) ($row->value ?? '')), 120) }}
</p>
@endif
@endif
</div>
</div>
@if (! $isEditing)
<flux:button size="sm" variant="ghost" icon="pencil"
wire:click="startFieldEdit({{ $editKey }})" />
@endif
</div>
</div>
@empty
<flux:text class="py-8 text-center">Keine Einträge gefunden.</flux:text>
@endforelse
</div>
</flux:card>
@else
<flux:card>
<div class="py-12 text-center">
<flux:icon name="document-text" class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
<flux:heading>Seite auswählen</flux:heading>
<flux:text>Wähle links eine Seite/Gruppe aus, um deren Inhalte zu bearbeiten.</flux:text>
</div>
</flux:card>
@endif
</div>
</div>
{{-- JSON-Editor: kein flux:modal — vermeidet ReferenceError fluxModal bei jedem Livewire-Render (z. B. bei legal_html). --}}
@if ($showJsonModal)
<div
class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6"
wire:key="cms-json-editor-overlay-{{ $selectedGroup ?? 'none' }}"
x-data
x-on:keydown.escape.window.prevent="$wire.cancelJsonModal()"
>
<div class="fixed inset-0 bg-zinc-950/50 backdrop-blur-sm" wire:click="cancelJsonModal" aria-hidden="true">
</div>
<div
role="dialog"
aria-modal="true"
class="relative z-10 flex w-full max-w-5xl max-h-[90vh] flex-col overflow-y-auto space-y-6 rounded-xl border border-zinc-200 bg-white p-6 shadow-xl dark:border-zinc-700 dark:bg-zinc-800"
wire:click.stop
>
<div>
<flux:heading size="lg">{{ $jsonEditingKey }}</flux:heading>
<flux:text class="mt-1">
{{ $jsonIsStringArray ? 'Einfache Liste' : 'Strukturierte Einträge' }}
({{ count($jsonItems) }} Einträge) — {{ strtoupper($editLocale) }}
</flux:text>
</div>
<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
</div>
<div class="space-y-4">
@foreach ($jsonItems as $idx => $item)
<div wire:key="json-item-{{ $idx }}"
class="rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
<div class="mb-3 flex items-center justify-between">
<span class="text-xs font-semibold text-zinc-500">Eintrag {{ $idx + 1 }}</span>
<flux:button size="xs" variant="ghost" icon="trash"
wire:click="removeJsonItem({{ $idx }})"
wire:confirm="Eintrag {{ $idx + 1 }} wirklich entfernen?" />
</div>
@if ($jsonIsStringArray)
<flux:input wire:model="jsonItems.{{ $idx }}._value" placeholder="Wert" />
@else
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
@foreach ($item as $field => $fieldValue)
@php
$isIcon = in_array($field, ['icon']);
$isRichText = in_array($field, ['description', 'text', 'content', 'help', 'answer', 'quote']);
$isNestedJson = is_string($fieldValue) && (str_starts_with($fieldValue, '[') || str_starts_with($fieldValue, '{'));
$isImageField = _cmsFieldLooksLikeImage($field, $fieldValue);
$jsonImageMediaId = is_string($fieldValue) && $fieldValue !== '' ? \FluxCms\Core\Models\CmsMedia::where('filename', $fieldValue)->first()?->id : null;
@endphp
@if ($isIcon)
<div class="md:col-span-2">
<div class="flex items-end gap-3">
<div class="flex-1">
<flux:select wire:model="jsonItems.{{ $idx }}.{{ $field }}"
variant="listbox" searchable label="{{ ucfirst($field) }}"
placeholder="Icon auswählen...">
<flux:select.option value="">— Kein Icon —</flux:select.option>
@foreach ($this->availableIcons as $iconName)
<flux:select.option value="{{ $iconName }}">{{ $iconName }}</flux:select.option>
@endforeach
</flux:select>
</div>
@if (! empty($fieldValue))
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-200 dark:border-zinc-700">
<x-dynamic-component :component="'heroicon-o-' . $fieldValue" class="h-5 w-5 text-primary" />
</div>
@endif
</div>
</div>
@elseif ($isImageField)
<div class="md:col-span-2">
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ ucfirst($field) }}</label>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start">
@if (is_string($fieldValue) && $fieldValue !== '')
<div class="h-20 w-32 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($fieldValue) }}" alt="" class="h-full w-full object-cover" />
</div>
@endif
<div class="min-w-0 flex-1 space-y-2">
<livewire:admin.cms.media-picker
:value="$jsonImageMediaId"
:field="'jsonimg:' . $idx . ':' . $field"
type="image"
profile="card"
label="Bild wählen"
:key="'json-img-' . ($selectedGroup ?? '') . '-' . $jsonEditingKey . '-' . $idx . '-' . $field . '-' . $editLocale"
/>
<flux:input wire:model="jsonItems.{{ $idx }}.{{ $field }}" size="sm" placeholder="Dateiname (optional manuell)" />
</div>
</div>
</div>
@elseif ($isRichText)
<div class="md:col-span-2">
<flux:editor wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }}" toolbar="bold italic highlight"
class="**:data-[slot=content]:min-h-[60px]!" />
</div>
@elseif ($isNestedJson)
<div class="md:col-span-2">
<flux:textarea wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }} (JSON)" rows="3"
class="font-mono text-xs" />
</div>
@else
<flux:input wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }}" />
@endif
@endforeach
</div>
@endif
</div>
@endforeach
</div>
<div class="flex items-center justify-between border-t border-zinc-200 dark:border-zinc-700 pt-4">
<flux:button size="sm" variant="ghost" icon="plus" wire:click="addJsonItem">
Eintrag hinzufügen
</flux:button>
<div class="flex gap-2">
<flux:button variant="ghost" wire:click="cancelJsonModal">Abbrechen</flux:button>
<flux:button variant="primary" wire:click="saveJsonModal">Speichern</flux:button>
</div>
</div>
</div>
</div>
@endif
</div>