FAQ im Inhalte-Editor vollständig bearbeitbar: Bereich je Frage statt verschachtelter sections

Die Bereiche der FAQ-Seite lagen als verschachtelte Listen in sections, der Editor zeigte
davon nur JSON-Text. Jetzt stehen alle Fragen in questions mit dem Feld category, Überschrift
und Symbol der Bereiche in categories. FAQ::groups baut daraus die Seite (allgemeine Fragen
zuerst), alte Einträge werden weiter dargestellt. FaqCategoriesSeeder stellt gespeicherte
Einträge um (FAQ und Immobilien-FAQ), Texte bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-09 09:50:44 +00:00
parent 69d50eed0f
commit dc31cdb401
8 changed files with 406 additions and 208 deletions

View file

@ -16,118 +16,68 @@
{{-- FAQ Container --}}
<div class="max-w-4xl mx-auto space-y-12">
{{-- Allgemeine Fragen (ungrouped) --}}
@if(!empty($content['questions']))
<div class="space-y-4">
@foreach($content['questions'] as $faq)
@php($index = $faqCounter++)
<div class="card-elevated rounded-xl overflow-hidden">
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="group flex w-full items-center justify-between p-6 text-left cursor-pointer"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6 transition-colors duration-300 group-hover:text-secondary">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-colors duration-300"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary'">
<svg class="w-5 h-5 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{ 'rotate-180': openIndex === {{ $index }} }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</button>
</dt>
{{-- grid-template-rows 0fr → 1fr animiert die tatsaechliche Hoehe. Der
zugeklappte Zustand steht auch statisch im class-Attribut, damit vor
dem Start von Alpine nichts aufgeklappt aufblitzt. --}}
<dd class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{
'grid-rows-[1fr] opacity-100': openIndex === {{ $index }},
'grid-rows-[0fr] opacity-0': openIndex !== {{ $index }},
}"
:aria-hidden="(openIndex !== {{ $index }}).toString()">
<div class="overflow-hidden">
<div class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
</div>
</div>
</dd>
</div>
@endforeach
</div>
@endif
{{-- Gruppierte Sektionen --}}
@if(!empty($content['sections']))
@foreach($content['sections'] as $section)
<div>
{{-- Sektions-Header --}}
{{-- Allgemeine Fragen zuerst, dann je Bereich Überschrift und Fragen (FAQ::groups) --}}
@foreach($groups as $group)
<div>
@if($group['title'] !== null)
<div class="flex items-center gap-3 mb-6">
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-secondary/10 text-secondary shrink-0">
@svg('heroicon-o-' . ($section['icon'] ?? 'question-mark-circle'), 'w-5 h-5')
@svg('heroicon-o-' . $group['icon'], 'w-5 h-5')
</div>
<h2 class="text-lg font-semibold text-foreground">{{ $section['title'] }}</h2>
<h2 class="text-lg font-semibold text-foreground">{{ $group['title'] }}</h2>
<div class="flex-1 h-px bg-border"></div>
</div>
@endif
<div class="space-y-4">
@foreach($section['questions'] as $faq)
@php($index = $faqCounter++)
<div class="card-elevated rounded-xl overflow-hidden">
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="group flex w-full items-center justify-between p-6 text-left cursor-pointer"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6 transition-colors duration-300 group-hover:text-secondary">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-colors duration-300"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary'">
<svg class="w-5 h-5 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{ 'rotate-180': openIndex === {{ $index }} }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</button>
</dt>
{{-- grid-template-rows 0fr → 1fr animiert die tatsaechliche Hoehe. Der
zugeklappte Zustand steht auch statisch im class-Attribut, damit vor
dem Start von Alpine nichts aufgeklappt aufblitzt. --}}
<dd class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{
'grid-rows-[1fr] opacity-100': openIndex === {{ $index }},
'grid-rows-[0fr] opacity-0': openIndex !== {{ $index }},
}"
:aria-hidden="(openIndex !== {{ $index }}).toString()">
<div class="overflow-hidden">
<div class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
<div class="space-y-4">
@foreach($group['questions'] as $faq)
@php($index = $faqCounter++)
<div class="card-elevated rounded-xl overflow-hidden">
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="group flex w-full items-center justify-between p-6 text-left cursor-pointer"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6 transition-colors duration-300 group-hover:text-secondary">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-colors duration-300"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary'">
<svg class="w-5 h-5 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{ 'rotate-180': openIndex === {{ $index }} }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</button>
</dt>
{{-- grid-template-rows 0fr → 1fr animiert die tatsaechliche Hoehe. Der
zugeklappte Zustand steht auch statisch im class-Attribut, damit vor
dem Start von Alpine nichts aufgeklappt aufblitzt. --}}
<dd class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{
'grid-rows-[1fr] opacity-100': openIndex === {{ $index }},
'grid-rows-[0fr] opacity-0': openIndex !== {{ $index }},
}"
:aria-hidden="(openIndex !== {{ $index }}).toString()">
<div class="overflow-hidden">
<div class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
</div>
</dd>
</div>
@endforeach
</div>
</div>
</dd>
</div>
@endforeach
</div>
@endforeach
@endif
</div>
@endforeach
@if(empty($content['questions']) && empty($content['sections']))
@if($groups === [])
<div class="text-center py-12">
<p class="text-muted-foreground">Keine FAQ-Inhalte verfügbar.</p>
</div>