Interior-Feinschliff: Hero-Slideshow, Lektorat, Rechtstexte, Aufräumen und Korrekturen 08.10.

Startseite und Heroes
- Hero-Slideshow (11 Motive inkl. Febrü) mit Infokarte, Laufzeit-Linie, größeren Klickflächen,
  Goldrahmen und mobiler Karte unter dem Bild; Zeile über der Überschrift, neue Vertrauenspunkte
- Hero-Stil auf Marken, Über uns und Kontakt übertragen, Breadcrumb entfernt
- Gründer-Leiste, Vision-Box auf Ocker, Showroom-Display in Box mit „Größer ansehen“/Vollbild
- Akzentfarbe #a8741f, größere Serif-Überschriften in der Interior-Welt
- Marken: Bildpause mit Zitat; Vereine aktualisiert und verlinkt

Inhalte
- Lektorat aller angezeigten Texte, /about-Korrekturen, Rechtstexte (DDG, TDDDG, Hosting Hetzner)
- Ablauf Maßanfertigung überall einheitlich, Magazin-Beitrag neu, alte Beiträge offline,
  deutsches Datum/Lesezeit, FAQ Projektausstattung, Otterpohl
- Referenzen-Seite per CMS-Schalter ausblendbar

CMS und Technik
- Tote Sektionen, Felder, Seiten und Komponenten entfernt und archiviert (dev/2026-08-28/archiv-*)
- Entwurf /entwurf, Farb-Schalter und /welcome entfernt; Test-Routen nur noch lokal
- Seitenbaum-Beschreibungen geprüft, fehlende CMS-Felder ergänzt, Markenliste aus „Kernmarken“
- Admin-Bildvorschauen repariert, verwendete Bilder in die Mediathek übernommen
- Seeder für das Deployment siehe dev/2026-08-28/entwicklungsplan.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-08 14:00:15 +00:00
parent 4b07bbdcae
commit fcb10be120
406 changed files with 22549 additions and 3798 deletions

View file

@ -1,13 +0,0 @@
<?php
namespace App\Livewire\Web\Components;
use Livewire\Component;
class AboutHero extends Component
{
public function render()
{
return view('livewire.web.components.sections.about-hero');
}
}

View file

@ -1,29 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class BenefitsSection extends Component
{
public $content = [];
public $layout = 'left'; // Standard-Layout
public $bg = 'bg-background';
public $section = 'card_section';
public function mount($layout = 'left', $bg = 'bg-background', $section = 'card_section')
{
$this->layout = $layout;
$this->bg = $bg;
$this->section = $section;
$this->content = cms_theme_section($this->section);
}
public function render()
{
return view('livewire.web.components.sections.benefits-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class BrokerSection extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('broker_section');
}
public function render()
{
return view('livewire.web.components.sections.broker-section');
}
}

View file

@ -1,29 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class CardSection extends Component
{
public $content = [];
public $layout = 'left'; // Standard-Layout
public $bg = 'bg-background';
public $section = 'card_section';
public function mount($layout = 'left', $bg = 'bg-background', $section = 'card_section')
{
$this->layout = $layout;
$this->bg = $bg;
$this->section = $section;
$this->content = cms_theme_section($this->section);
}
public function render()
{
return view('livewire.web.components.sections.card-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class CommitmentSection extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('commitment_section');
}
public function render()
{
return view('livewire.web.components.sections.commitment-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class DarkStatsSection extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('dark_stats_section');
}
public function render()
{
return view('livewire.web.components.sections.dark-stats-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class DigitalCore extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('digital_core');
}
public function render()
{
return view('livewire.web.components.sections.digital-core');
}
}

View file

@ -29,7 +29,7 @@ class DisplayFeed extends Component
$content = is_array($section) ? $section : [];
$this->bg = $bg;
$this->showLink = $showLink;
$this->showLink = $showLink && $this->linkTargetVisible($content);
$this->displayId = $this->resolveDisplayId($content);
// Ohne Display bleiben auch die Texte leer – sie hätten sonst nichts zu beschriften und
@ -69,4 +69,16 @@ class DisplayFeed extends Component
{
return view('livewire.web.components.sections.display-feed');
}
/**
* Der Button führt standardmäßig auf /referenzen – ist die Seite im CMS ausgeblendet, entfällt er.
*
* @param array<string, mixed> $content
*/
private function linkTargetVisible(array $content): bool
{
$path = rtrim(parse_url((string) ($content['link_url'] ?? '/referenzen'), PHP_URL_PATH) ?? '', '/');
return $path !== '/referenzen' || references_page_enabled();
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class EcosystemHero extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('ecosystem_hero');
}
public function render()
{
return view('livewire.web.components.sections.ecosystem-hero');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class EcosystemStats extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('ecosystem_stats');
}
public function render()
{
return view('livewire.web.components.sections.ecosystem-stats');
}
}

View file

@ -1,26 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class EndCustomerSection extends Component
{
public $content = [];
public $bg = '';
public $section = '';
public function mount($bg = 'bg-accent', $section = 'end_customer_section')
{
$this->section = $section;
$this->bg = $bg;
$this->content = cms_theme_section('end_customer_section');
}
public function render()
{
return view('livewire.web.components.sections.end-customer-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class FinalCommitment extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('final_commitment');
}
public function render()
{
return view('livewire.web.components.sections.final-commitment');
}
}

View file

@ -8,8 +8,12 @@ class FounderBar extends Component
{
public array $content = [];
public function mount(): void
/** Hintergrundklasse der Sektion; unter den Interior-Heroes (Startseite, Marken, Über uns) `bg-accent`. */
public string $bg = 'bg-background';
public function mount(string $bg = 'bg-background'): void
{
$this->bg = $bg;
$this->content = cms_theme_section('founder_bar');
}

View file

@ -4,16 +4,86 @@ namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
/**
* Hero der Startseite: Text links, rechts eine ruhige Slideshow als Hauptelement des Rahmens
* (Abstimmung 07.10.2026, Vorbild dev/2026-08-28/Farbwelt und Stil.png).
*
* Die Bilder kommen aus der Liste `slides` der Sektion `home/hero` (je `image`, `label`, `caption`,
* `alt`, `active`) und laufen in dieser Reihenfolge. Die Infokarte über dem Bild zeigt je Slide
* Marke (`label`) und Motiv (`caption`), ohne Angaben die festen Texte `card_title`/`card_text`.
* Ist die Liste leer oder alles deaktiviert, steht das einzelne Bild `image` da wie früher.
*/
class Hero extends Component
{
public $content = [];
/** @var array<string, mixed> */
public array $content = [];
public function mount()
/** @var array<int, array{image: string, alt: string, label: string, caption: string}> */
public array $slides = [];
public function mount(): void
{
$this->content = cms_theme_section('hero');
$section = cms_theme_section('hero');
$this->content = is_array($section) ? $section : [];
$this->slides = self::activeSlides($this->content['slides'] ?? []) ?: $this->singleImage();
}
public function render()
/**
* Gepflegte Slides ohne Bild oder mit `active` = aus fallen weg.
*
* @return array<int, array{image: string, alt: string, label: string, caption: string}>
*/
public static function activeSlides(mixed $list): array
{
$slides = [];
foreach ((array) $list as $slide) {
if (! is_array($slide) || trim((string) ($slide['image'] ?? '')) === '') {
continue;
}
if (array_key_exists('active', $slide) && ! filter_var($slide['active'], FILTER_VALIDATE_BOOLEAN)) {
continue;
}
$label = trim((string) ($slide['label'] ?? $slide['title'] ?? ''));
$caption = trim((string) ($slide['caption'] ?? ''));
$slides[] = [
'image' => (string) $slide['image'],
'alt' => trim((string) ($slide['alt'] ?? '')) ?: implode(' – ', array_filter([$label, $caption])),
'label' => $label,
'caption' => $caption,
];
}
return $slides;
}
/**
* @return array<int, array{image: string, alt: string, label: string, caption: string}>
*/
private function singleImage(): array
{
$image = trim((string) ($this->content['image'] ?? ''));
return $image === '' ? [] : [[
'image' => $image,
'alt' => (string) ($this->content['image_alt'] ?? ''),
'label' => '',
'caption' => '',
]];
}
/** Wechsel alle `interval` Sekunden, begrenzt auf 3–30 s (Standard 7 s). */
public function getIntervalMsProperty(): int
{
$seconds = (float) ($this->content['interval'] ?? 7);
return (int) round(max(3, min(30, $seconds)) * 1000);
}
public function render(): \Illuminate\View\View
{
return view('livewire.web.components.sections.hero');
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class HeroImage extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('hero_image');
}
public function render()
{
return view('livewire.web.components.sections.hero-image');
}
}

View file

@ -2,96 +2,47 @@
namespace App\Livewire\Web\Components\Sections;
use App\Models\CmsBrand;
use Livewire\Component;
/**
* Hero des Startseiten-Entwurfs (Nachtrag 19, Briefing Marcel 24.09.): Rahmen mit Goldkante,
* eine Überschrift, ein Button und statt des festen Bildes eine ruhige Slideshow.
*
* Die Slides kommen aus der Sektion `home/hero_slider` (Liste `slides`), sobald Marcel dort
* welche pflegt. Solange die Liste leer ist, laufen die Titelbilder der veröffentlichten
* Kernmarken in ihrer Reihenfolge – Marken ohne Bild fallen weg.
* Hero der Themes b2a, Stileigentum und Style2own (Stand vor dem B2in-Entwurf vom 24.09.2026).
* Ohne gepflegte Sektion `hero_slider` bleibt der Bereich leer.
*/
class HeroSlider extends Component
{
/** @var array<string, mixed> */
public array $content = [];
public $content = [];
/** @var array<int, array{image: string, alt: string, label: string, caption: string}> */
public array $slides = [];
public $currentSlide = 0;
public function mount(): void
public function mount()
{
$section = cms_theme_section('hero_slider');
$this->content = is_array($section) ? $section : [];
$this->slides = $this->slidesFromContent() ?: $this->slidesFromBrands();
}
/**
* @return array<int, array{image: string, alt: string, label: string, caption: string}>
*/
private function slidesFromContent(): array
public function nextSlide()
{
$slides = [];
foreach ((array) ($this->content['slides'] ?? []) as $slide) {
if (! is_array($slide) || trim((string) ($slide['image'] ?? '')) === '') {
continue;
}
if (array_key_exists('active', $slide) && ! filter_var($slide['active'], FILTER_VALIDATE_BOOLEAN)) {
continue;
}
$label = trim((string) ($slide['label'] ?? $slide['title'] ?? ''));
$slides[] = [
'image' => (string) $slide['image'],
'alt' => trim((string) ($slide['alt'] ?? $label)),
'label' => $label,
'caption' => trim((string) ($slide['caption'] ?? '')),
];
if (! empty($this->content['slides'])) {
$this->currentSlide = ($this->currentSlide + 1) % count($this->content['slides']);
}
return $slides;
}
/**
* @return array<int, array{image: string, alt: string, label: string, caption: string}>
*/
private function slidesFromBrands(): array
public function previousSlide()
{
$slides = [];
foreach (CmsBrand::forFrontend() as $brand) {
$image = trim((string) ($brand['image'] ?? ''));
if ($image === '') {
continue;
}
$name = (string) ($brand['name'] ?? '');
$slides[] = [
'image' => $image,
'alt' => $name.' – '.strip_tags((string) ($brand['tagline'] ?? '')),
'label' => $name,
'caption' => strip_tags((string) ($brand['tagline'] ?? '')),
];
if (! empty($this->content['slides'])) {
$totalSlides = count($this->content['slides']);
$this->currentSlide = ($this->currentSlide - 1 + $totalSlides) % $totalSlides;
}
return $slides;
}
public function getIntervalMsProperty(): int
public function setSlide($index)
{
$seconds = (float) ($this->content['interval'] ?? 7);
return (int) round(max(3, min(30, $seconds)) * 1000);
if (! empty($this->content['slides']) && $index >= 0 && $index < count($this->content['slides'])) {
$this->currentSlide = $index;
}
}
public function render(): \Illuminate\View\View
public function render()
{
return view('livewire.web.components.sections.hero-slider');
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class HeroTiles extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('hero_tiles');
}
public function render()
{
return view('livewire.web.components.sections.hero-tiles');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class LeadershipTeam extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('leadership_team');
}
public function render()
{
return view('livewire.web.components.sections.leadership-team');
}
}

View file

@ -5,7 +5,8 @@ namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
/**
* Seitenkopf für Unterseiten: Breadcrumb, Überschrift, Text und optional ein Bild daneben.
* Seitenkopf für Unterseiten: Zeile über der Überschrift, Überschrift, Text und optional ein Bild
* daneben (Breadcrumb seit 07.10.2026 entfernt).
*
* Liegt auf derselben abgesetzten grauen Fläche wie der Hero der Startseite, damit sich der
* Kopf klar von den Inhaltsblöcken darunter abhebt.
@ -17,13 +18,10 @@ class PageHero extends Component
public string $bg = '';
public string $breadcrumb = '';
public function mount(string $section = 'marken_hero', string $breadcrumb = '', string $bg = ''): void
public function mount(string $section = 'marken_hero', string $bg = ''): void
{
$content = cms_theme_section($section);
$this->content = is_array($content) ? $content : [];
$this->breadcrumb = $breadcrumb !== '' ? $breadcrumb : __('ui.brands');
$this->bg = $bg;
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class PartnerBenefits extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('partner_benefits');
}
public function render()
{
return view('livewire.web.components.sections.partner-benefits');
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class PartnerHero extends Component
{
public $partnerTypes = [
[
'title' => 'Makler',
'description' => 'Lifetime-Vergütung',
'icon' => 'trending-up',
],
[
'title' => 'Lieferanten',
'description' => 'Globale Märkte',
'icon' => 'globe',
],
[
'title' => 'Partnership',
'description' => 'Faire Konditionen',
'icon' => 'handshake',
],
[
'title' => 'Erfolg',
'description' => 'Nachhaltiges Wachstum',
'icon' => 'award',
],
];
public $section = 'partner_hero';
public $content = [];
public function mount($section = 'partner_hero')
{
$this->section = $section;
$this->content = cms_theme_section($this->section);
}
public function render()
{
return view('livewire.web.components.sections.partner-hero', [
'partnerTypes' => $this->partnerTypes,
]);
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class PartnerProcess extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('partner_process');
}
public function render()
{
return view('livewire.web.components.sections.partner-process');
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class Portfolio extends Component
{
public string $activeFilter = 'alle';
public ?array $selectedProject = null;
public bool $showModal = false;
public $content = [];
public $theme = '';
public $filters = [];
public function mount()
{
$filters = [
'alle' => 'Alle',
'villen' => 'Villen',
'penthouse' => 'Penthouse',
'loft' => 'Loft',
];
$this->filters = $filters;
$this->activeFilter = 'alle';
$this->theme = config('app.theme', 'b2in');
$portfolio = cms_theme_section('portfolio', $this->theme);
if (! is_array($portfolio)) {
$portfolio = [];
}
$this->content = $portfolio;
$this->filters = $portfolio['filters'] ?? $filters;
}
public function filterBy(string $category): void
{
$this->activeFilter = $category;
}
public function openModal(array $project): void
{
$this->selectedProject = $project;
$this->showModal = true;
}
public function closeModal(): void
{
$this->showModal = false;
$this->selectedProject = null;
}
public function getFilteredProjects(): array
{
$projects = $this->content['projects'] ?? [];
if ($this->activeFilter === 'alle') {
return $projects;
}
return array_filter($projects, function ($project) {
return strtolower($project['category']) === strtolower($this->activeFilter);
});
}
public function render()
{
return view('livewire.web.components.sections.portfolio');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class SpotlightsSection extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('spotlights_section');
}
public function render()
{
return view('livewire.web.components.sections.spotlights-section');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Livewire\Web\Components\Sections;
use Livewire\Component;
class SupplierSection extends Component
{
public $content = [];
public function mount()
{
$this->content = cms_theme_section('supplier_section');
}
public function render()
{
return view('livewire.web.components.sections.supplier-section');
}
}

View file

@ -10,9 +10,13 @@ class VisionSection extends Component
public $bg = 'bg-background';
public function mount($bg = 'bg-background')
/** Inhalt in weißer Box mit kleinerem Bild (Startseite, 07.10.2026); die anderen Themes bleiben offen. */
public bool $boxed = false;
public function mount($bg = 'bg-background', bool $boxed = false)
{
$this->bg = $bg;
$this->boxed = $boxed;
$this->content = cms_theme_section('vision_section');
}

View file

@ -147,12 +147,6 @@ class ContactForm extends Component
return $this->content['contact_info'] ?? [];
}
/** @return array<int, array<string, mixed>> */
public function getSocialMediaProperty(): array
{
return $this->content['social_media']['platforms'] ?? [];
}
public function submit(ContactFormService $service): void
{
$this->validate([

View file

@ -51,6 +51,14 @@ class Header extends Component
if (!isset($this->content['navigation']) || !is_array($this->content['navigation'])) {
$this->content['navigation'] = [];
}
// Referenzen lassen sich im CMS ausblenden; dann verschwindet auch der Menüpunkt.
if (! references_page_enabled()) {
$this->content['navigation'] = array_values(array_filter(
$this->content['navigation'],
fn ($item) => rtrim(parse_url((string) ($item['url'] ?? ''), PHP_URL_PATH) ?? '', '/') !== '/referenzen',
));
}
}
public function switchLanguage(string $locale): mixed