782 lines
38 KiB
PHP
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>
|