- 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>
387 lines
12 KiB
PHP
387 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Spatie\Translatable\HasTranslations;
|
||
|
||
/**
|
||
* Kernmarke mit eigener Detailseite (CABINET, Rotpunkt Küchen, Febrü …).
|
||
*
|
||
* Nicht zu verwechseln mit {@see Brand} – das ist die Produkt-/Partner-Marke.
|
||
*/
|
||
class CmsBrand extends Model
|
||
{
|
||
use HasFactory, HasTranslations;
|
||
|
||
/** Tatsaechlich ausgefuehrtes Projekt – ein Foto aus einem echten Auftrag. */
|
||
public const GALLERY_PROJECT = 'project';
|
||
|
||
/** Entwurf, Rendering oder KI-gestuetzte Darstellung – nicht realisiert. */
|
||
public const GALLERY_INSPIRATION = 'inspiration';
|
||
|
||
/** Einblick in Werkstatt, Fertigung oder Montage des Herstellers – weder Projekt noch Entwurf (Nachtrag 19). */
|
||
public const GALLERY_COMPANY = 'company';
|
||
|
||
/** @var list<string> */
|
||
public const GALLERY_CATEGORIES = [self::GALLERY_PROJECT, self::GALLERY_INSPIRATION, self::GALLERY_COMPANY];
|
||
|
||
protected $table = 'cms_brands';
|
||
|
||
protected $fillable = [
|
||
'slug',
|
||
'name',
|
||
'tagline',
|
||
'teaser',
|
||
'summary',
|
||
'highlights',
|
||
'description',
|
||
'logo',
|
||
'image',
|
||
'services',
|
||
'sections',
|
||
'widget_label',
|
||
'cta_title',
|
||
'cta_text',
|
||
'gallery',
|
||
'website_url',
|
||
'showroom',
|
||
'origin_city',
|
||
'origin_distance',
|
||
'origin_role',
|
||
'is_published',
|
||
'order',
|
||
];
|
||
|
||
/** @var array<string> */
|
||
public array $translatable = [
|
||
'name',
|
||
'tagline',
|
||
'teaser',
|
||
'summary',
|
||
'description',
|
||
'widget_label',
|
||
'cta_title',
|
||
'cta_text',
|
||
'origin_city',
|
||
'origin_distance',
|
||
'origin_role',
|
||
];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'highlights' => 'array',
|
||
'services' => 'array',
|
||
'sections' => 'array',
|
||
'gallery' => 'array',
|
||
'is_published' => 'boolean',
|
||
'order' => 'integer',
|
||
];
|
||
}
|
||
|
||
public function scopePublished(Builder $query): Builder
|
||
{
|
||
return $query->where('is_published', true);
|
||
}
|
||
|
||
public function scopeOrdered(Builder $query): Builder
|
||
{
|
||
return $query->orderBy('order')->orderBy('id');
|
||
}
|
||
|
||
/**
|
||
* Veröffentlichte Kernmarken fürs Frontend, nach Slug indiziert.
|
||
*
|
||
* Das CMS gewinnt. Solange keine Marke gepflegt ist, greift die Lang-Sektion `core_brands`
|
||
* als Fallback – dieselbe Reihenfolge wie bei cms_theme_section(), damit die Startseite auf
|
||
* einer frischen Installation nicht ohne Kernmarken dasteht.
|
||
*
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
public static function forFrontend(): array
|
||
{
|
||
$brands = static::query()->published()->ordered()->get();
|
||
|
||
if ($brands->isNotEmpty()) {
|
||
return $brands
|
||
->mapWithKeys(fn (self $brand) => [$brand->slug => $brand->toFrontendArray()])
|
||
->all();
|
||
}
|
||
|
||
return static::fallbackFromLang();
|
||
}
|
||
|
||
/**
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
private static function fallbackFromLang(): array
|
||
{
|
||
$section = cms_theme_section('core_brands');
|
||
$brands = is_array($section) ? ($section['brands'] ?? []) : [];
|
||
|
||
$out = [];
|
||
|
||
foreach ($brands as $brand) {
|
||
if (! is_array($brand) || empty($brand['name'])) {
|
||
continue;
|
||
}
|
||
|
||
$slug = $brand['slug'] ?? \Illuminate\Support\Str::slug((string) $brand['name']);
|
||
|
||
$entry = array_merge([
|
||
'slug' => $slug,
|
||
'tagline' => '',
|
||
'teaser' => '',
|
||
'summary' => '',
|
||
'highlights' => [],
|
||
'description' => '',
|
||
'logo' => '',
|
||
'image' => '',
|
||
'services' => [],
|
||
'sections' => [],
|
||
'widget_label' => '',
|
||
'cta_title' => '',
|
||
'cta_text' => '',
|
||
'gallery' => [],
|
||
'website_url' => '',
|
||
'showroom' => '',
|
||
'origin_city' => '',
|
||
'origin_distance' => '',
|
||
'origin_role' => '',
|
||
'has_detail_page' => false,
|
||
], $brand, ['slug' => $slug, 'has_detail_page' => false]);
|
||
|
||
$entry['gallery'] = self::normalizeGallery($entry['gallery']);
|
||
$entry['sections'] = self::normalizeSections($entry['sections']);
|
||
|
||
$out[$slug] = $entry;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* Absätze eines Abschnitts für die Ausgabe gruppieren.
|
||
*
|
||
* Ein Absatz, der mit „- “ beginnt, ist ein Listenpunkt; aufeinanderfolgende Punkte bilden
|
||
* eine Liste. Steht im Punkt ein Doppelpunkt, wird der Teil davor als fette Einleitung
|
||
* ausgegeben („- Sichere Prüfung & Terminierung: Wir vereinbaren …“). So lässt sich eine
|
||
* Aufzählung im Backend im normalen Textfeld pflegen.
|
||
*
|
||
* @param list<string> $paragraphs
|
||
* @return list<array{type: 'paragraph', text: string}|array{type: 'list', items: list<array{lead: string, text: string}>}>
|
||
*/
|
||
public static function paragraphBlocks(array $paragraphs): array
|
||
{
|
||
$blocks = [];
|
||
|
||
foreach ($paragraphs as $paragraph) {
|
||
if (! preg_match('/^[-–•]\s+(.+)$/su', $paragraph, $match)) {
|
||
$blocks[] = ['type' => 'paragraph', 'text' => $paragraph];
|
||
|
||
continue;
|
||
}
|
||
|
||
[$lead, $text] = str_contains($match[1], ': ')
|
||
? array_map('trim', explode(': ', $match[1], 2))
|
||
: ['', trim($match[1])];
|
||
|
||
$last = array_key_last($blocks);
|
||
|
||
if ($last === null || $blocks[$last]['type'] !== 'list') {
|
||
$blocks[] = ['type' => 'list', 'items' => []];
|
||
$last = array_key_last($blocks);
|
||
}
|
||
|
||
$blocks[$last]['items'][] = ['lead' => $lead, 'text' => $text];
|
||
}
|
||
|
||
return $blocks;
|
||
}
|
||
|
||
/**
|
||
* @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.
|
||
*
|
||
* Ein Eintrag ohne (oder mit unbekannter) Kategorie gilt als Inspiration, nicht als Projekt:
|
||
* Ein Entwurf, der versehentlich als Referenz durchgeht, waere die schaedlichere Verwechslung.
|
||
*
|
||
* @param mixed $gallery
|
||
* @return list<array{image: string, category: string, caption: string}>
|
||
*/
|
||
public static function normalizeGallery($gallery): array
|
||
{
|
||
if (! is_array($gallery)) {
|
||
return [];
|
||
}
|
||
|
||
$out = [];
|
||
|
||
foreach ($gallery as $item) {
|
||
$image = is_array($item) ? (string) ($item['image'] ?? '') : (string) $item;
|
||
$image = trim($image);
|
||
|
||
if ($image === '') {
|
||
continue;
|
||
}
|
||
|
||
$category = is_array($item) ? (string) ($item['category'] ?? '') : '';
|
||
|
||
$out[] = [
|
||
'image' => $image,
|
||
'category' => in_array($category, self::GALLERY_CATEGORIES, true)
|
||
? $category
|
||
: self::GALLERY_INSPIRATION,
|
||
'caption' => is_array($item) ? trim((string) ($item['caption'] ?? '')) : '',
|
||
];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* Galerie nach realisierten Projekten, Einblicken in die Werkstatt und Inspirationen getrennt.
|
||
*
|
||
* @param mixed $gallery
|
||
* @return array{projects: list<array{image: string, category: string, caption: string}>, company: list<array{image: string, category: string, caption: string}>, inspirations: list<array{image: string, category: string, caption: string}>}
|
||
*/
|
||
public static function groupGallery($gallery): array
|
||
{
|
||
$items = self::normalizeGallery($gallery);
|
||
|
||
return [
|
||
'projects' => array_values(array_filter(
|
||
$items,
|
||
fn (array $item) => $item['category'] === self::GALLERY_PROJECT,
|
||
)),
|
||
'company' => array_values(array_filter(
|
||
$items,
|
||
fn (array $item) => $item['category'] === self::GALLERY_COMPANY,
|
||
)),
|
||
'inspirations' => array_values(array_filter(
|
||
$items,
|
||
fn (array $item) => $item['category'] === self::GALLERY_INSPIRATION,
|
||
)),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Beschriftung des Badges auf dem Galeriebild („Realisiertes Projekt“, „Aus der Werkstatt“, „Visualisierung“).
|
||
*/
|
||
public static function galleryBadge(string $category): string
|
||
{
|
||
return match ($category) {
|
||
self::GALLERY_PROJECT => __('ui.brand_gallery_badge_project'),
|
||
self::GALLERY_COMPANY => __('ui.brand_gallery_badge_company'),
|
||
default => __('ui.brand_gallery_badge_inspiration'),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function toFrontendArray(): array
|
||
{
|
||
return [
|
||
'id' => $this->id,
|
||
'slug' => $this->slug,
|
||
'name' => $this->name,
|
||
'tagline' => $this->tagline,
|
||
'teaser' => $this->teaser,
|
||
'summary' => $this->summary,
|
||
'highlights' => $this->highlights ?? [],
|
||
'description' => $this->description,
|
||
'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,
|
||
'origin_city' => (string) $this->origin_city,
|
||
'origin_distance' => (string) $this->origin_distance,
|
||
'origin_role' => (string) $this->origin_role,
|
||
'has_detail_page' => true,
|
||
];
|
||
}
|
||
}
|