Markenseiten Rotpunkt und Febrü aus der Zulieferung vom 31.08.
Umsetzung der beiden finalen Webseitentexte aus dev/2026-08-28/. Die angekündigte Aufgabenliste umsetzung-03-09-2026.md war leer, Grundlage sind daher die Umsetzungshinweise in den PDFs selbst. - Neue Felder in cms_brands: sections, widget_label, cta_title, cta_text. Einleitung und Leistungskacheln reichten für diese Texte nicht aus. - Die als Vertiefung gelieferten Blöcke werden aufklappbar dargestellt, mit aria-expanded und aria-controls. Bewusst x-transition statt x-collapse, weil das Collapse-Plugin hier nicht registriert ist. - Kein zweiter CTA-Button: das schwebende Terminanfrage-Widget trägt auf der Markenseite die Beschriftung der Marke und liest den Kontext aus der Route, da es im Layout und nicht je Seite eingebunden ist. - Footer bekommt unten Platz und lange Beschriftungen brechen um, damit das Widget auf dem Smartphone nichts verdeckt. - Abschnitte sind im Admin vollständig pflegbar, inklusive Sortierung. - Interne Quellenhinweise der PDFs sind nicht übernommen; ein Test prüft, dass sie nicht im Ausgabe-HTML stehen. - BrandPageTextSeeder fasst nur die Textfelder an, Bilder, Galerie und Leistungen bleiben unangetastet. Vor der Veröffentlichung: Herstellerfreigabe einholen und die Aussage zur erhaltenen Waldfläche nur mit ausdrücklicher Bestätigung verwenden. Beides steht in der Deployment-Checkliste. 854 Tests grün (3150 Assertions), Pint sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6d53243ea6
commit
4ef3a4d8f1
13 changed files with 1143 additions and 69 deletions
|
|
@ -38,6 +38,10 @@ class CmsBrand extends Model
|
|||
'logo',
|
||||
'image',
|
||||
'services',
|
||||
'sections',
|
||||
'widget_label',
|
||||
'cta_title',
|
||||
'cta_text',
|
||||
'gallery',
|
||||
'website_url',
|
||||
'showroom',
|
||||
|
|
@ -52,6 +56,9 @@ class CmsBrand extends Model
|
|||
'teaser',
|
||||
'summary',
|
||||
'description',
|
||||
'widget_label',
|
||||
'cta_title',
|
||||
'cta_text',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
|
|
@ -59,6 +66,7 @@ class CmsBrand extends Model
|
|||
return [
|
||||
'highlights' => 'array',
|
||||
'services' => 'array',
|
||||
'sections' => 'array',
|
||||
'gallery' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'order' => 'integer',
|
||||
|
|
@ -124,6 +132,10 @@ class CmsBrand extends Model
|
|||
'logo' => '',
|
||||
'image' => '',
|
||||
'services' => [],
|
||||
'sections' => [],
|
||||
'widget_label' => '',
|
||||
'cta_title' => '',
|
||||
'cta_text' => '',
|
||||
'gallery' => [],
|
||||
'website_url' => '',
|
||||
'showroom' => '',
|
||||
|
|
@ -131,6 +143,7 @@ class CmsBrand extends Model
|
|||
], $brand, ['slug' => $slug, 'has_detail_page' => false]);
|
||||
|
||||
$entry['gallery'] = self::normalizeGallery($entry['gallery']);
|
||||
$entry['sections'] = self::normalizeSections($entry['sections']);
|
||||
|
||||
$out[$slug] = $entry;
|
||||
}
|
||||
|
|
@ -138,6 +151,81 @@ class CmsBrand extends Model
|
|||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seitenabschnitte in eine feste Form bringen.
|
||||
*
|
||||
* Jeder Abschnitt hat einen Titel und Absätze. Optional dazu:
|
||||
* - `claim` hervorgehobene Schlusszeile
|
||||
* - `quote` Zitat mit `quote_author`
|
||||
* - `detail` Vertiefung, die im Frontend aufklappbar dargestellt wird
|
||||
*
|
||||
* @param mixed $sections
|
||||
* @return list<array{title: string, paragraphs: list<string>, claim: string, quote: string, quote_author: string, detail: array{title: string, paragraphs: list<string>}|null}>
|
||||
*/
|
||||
public static function normalizeSections($sections): array
|
||||
{
|
||||
if (! is_array($sections)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
|
||||
foreach ($sections as $section) {
|
||||
if (! is_array($section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paragraphs = self::paragraphList($section['paragraphs'] ?? []);
|
||||
$title = trim((string) ($section['title'] ?? ''));
|
||||
|
||||
if ($title === '' && $paragraphs === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$detail = null;
|
||||
|
||||
if (is_array($section['detail'] ?? null)) {
|
||||
$detailParagraphs = self::paragraphList($section['detail']['paragraphs'] ?? []);
|
||||
$detailTitle = trim((string) ($section['detail']['title'] ?? ''));
|
||||
|
||||
if ($detailTitle !== '' || $detailParagraphs !== []) {
|
||||
$detail = ['title' => $detailTitle, 'paragraphs' => $detailParagraphs];
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'title' => $title,
|
||||
'paragraphs' => $paragraphs,
|
||||
'claim' => trim((string) ($section['claim'] ?? '')),
|
||||
'quote' => trim((string) ($section['quote'] ?? '')),
|
||||
'quote_author' => trim((string) ($section['quote_author'] ?? '')),
|
||||
'detail' => $detail,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $paragraphs
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function paragraphList($paragraphs): array
|
||||
{
|
||||
if (is_string($paragraphs)) {
|
||||
$paragraphs = preg_split('/\R{2,}/', $paragraphs) ?: [];
|
||||
}
|
||||
|
||||
if (! is_array($paragraphs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
fn ($p) => trim((string) (is_array($p) ? ($p['text'] ?? '') : $p)),
|
||||
$paragraphs,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Galerie-Eintraege in eine feste Form bringen.
|
||||
*
|
||||
|
|
@ -216,6 +304,10 @@ class CmsBrand extends Model
|
|||
'logo' => $this->logo,
|
||||
'image' => $this->image,
|
||||
'services' => $this->services ?? [],
|
||||
'sections' => self::normalizeSections($this->sections),
|
||||
'widget_label' => (string) $this->widget_label,
|
||||
'cta_title' => (string) $this->cta_title,
|
||||
'cta_text' => (string) $this->cta_text,
|
||||
'gallery' => self::normalizeGallery($this->gallery),
|
||||
'website_url' => $this->website_url,
|
||||
'showroom' => $this->showroom,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue