diff --git a/.env.example b/.env.example index 4ffd7b09..c9d9b6a9 100644 --- a/.env.example +++ b/.env.example @@ -80,3 +80,8 @@ DISPLAY_PLAYER_URL=https://portal.b2in.test/_cabinet/display # Nützlich in der Entwicklung, um Seed-Daten und manuelle Testeinträge zu erhalten. # Hinweis: Migrationen müssen mindestens einmal für die Test-DB ausgeführt werden: DB_DATABASE=testing sail artisan migrate # TESTING_PRESERVE_DATABASE=false + +# Seed-Admin (DatabaseSeeder) +SEED_ADMIN_NAME="Admin" +SEED_ADMIN_EMAIL=admin@b2in.test +SEED_ADMIN_PASSWORD= diff --git a/.gitignore b/.gitignore index 8b98b037..9b26cb51 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,14 @@ Icon _static/ _work/ _storage/ -dev/immobilien 07-05-2026/AZIZI SharePoint \ No newline at end of file +dev/immobilien 07-05-2026/AZIZI SharePoint +# Deployment-Archive werden bei Bedarf neu erzeugt +dev/**/deploy/*.tar.gz + +# Referenzmaterial und Projektdokumente ausserhalb der Versionierung +/_b2in_interior_shop/ +/docs/ + +# Original-Bildzulieferungen (mehrere MB je Datei) bleiben ausserhalb der Versionierung; +# die optimierten WebP-Fassungen liegen unter public/img/assets/marken/. +/dev/2026-08-28/images/ diff --git a/CLAUDE.md b/CLAUDE.md index ed7a30e0..3fb45724 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,7 +128,7 @@ Domains are configured in `config/domains.php` with environment variables: Domain-specific configurations include: - **portal.b2in.test**: Admin backend with Flux UI theme -- **b2in.test**: Anthracite/Dynamic Blue color scheme, Inter/IBM Plex Sans fonts +- **b2in.test**: two color worlds on one theme – Interior (default: Tanne `#202c2b`, Creme `#f7f4ef`, Sand `#d9cbbf`, Ocker `#d39e46`, Clay `#a38f82`, Anthrazit `#2d2926`; Inter/EB Garamond) and Immobilien (`body.world-immobilien`: Anthracite/Dynamic Blue, Inter/IBM Plex Sans). The world comes from `b2in_world()` (app/helpers.php), the logo per world from `ThemeHelper::getLogoPath()` - **b2a.test**: Azur Blue/Liberty Red color scheme, Inter/Merriweather fonts - **stileigentum.test**: Style Blue/Style Sun color scheme, Inter/Ephesis fonts - **style2own.test**: Imperial Blue/Sand Gold color scheme, Inter/EB Garamond fonts diff --git a/app/Helpers/ThemeHelper.php b/app/Helpers/ThemeHelper.php index 4d17ba6e..adf0b89e 100644 --- a/app/Helpers/ThemeHelper.php +++ b/app/Helpers/ThemeHelper.php @@ -5,11 +5,25 @@ namespace App\Helpers; class ThemeHelper { /** - * Get the logo path based on the current theme + * Logo-Pfad für das aktuelle Theme. + * + * Für b2in hängt die Datei zusätzlich an der Farbwelt: die Interior-Welt trägt das Logo + * mit dem Zusatz „Interior“ und ockerfarbenem Bogen, die Immobilien-Welt das Logo ohne + * Zusatz mit blauem Bogen. Ohne Angabe wird die Welt aus der aktuellen Route gelesen. */ - public static function getLogoPath(string $type = 'positive'): string + public static function getLogoPath(string $type = 'positive', ?string $world = null): string { $theme = config('app.theme', 'b2in'); + + if ($theme === 'b2in' && ($world ?? b2in_world()) === 'immobilien') { + return self::getLogoPathForBrand('b2in-immobilien', $type); + } + + // Entwurf in der Farbwelt vom 24.09. (Nachtrag 19): Bogen in Gold statt Ocker. + if ($theme === 'b2in' && b2in_palette() === 'greige') { + return "img/logos/b2in-interior-logo-gold-{$type}.svg"; + } + return self::getLogoPathForBrand($theme, $type); } @@ -22,21 +36,25 @@ class ThemeHelper $logoMap = [ 'b2in' => [ + 'positive' => 'img/logos/b2in-interior-logo-positive.svg', + 'negative' => 'img/logos/b2in-interior-logo-negative.svg', + ], + 'b2in-immobilien' => [ 'positive' => 'img/logos/b2in-logo-positive.svg', - 'negative' => 'img/logos/b2in-logo-negative.svg' + 'negative' => 'img/logos/b2in-logo-negative.svg', ], 'b2a' => [ 'positive' => 'img/logos/b2a-logo-positiv.svg', - 'negative' => 'img/logos/b2a-logo-negativ.svg' + 'negative' => 'img/logos/b2a-logo-negativ.svg', ], 'stileigentum' => [ 'positive' => 'img/logos/stileigentum-logo-positiv.svg', - 'negative' => 'img/logos/stileigentum-logo-negativ.svg' + 'negative' => 'img/logos/stileigentum-logo-negativ.svg', ], 'style2own' => [ 'positive' => 'img/logos/style2own-logo-positiv.svg', - 'negative' => 'img/logos/style2own-logo-negativ.svg' - ] + 'negative' => 'img/logos/style2own-logo-negativ.svg', + ], ]; return $logoMap[$brand][$type] ?? $logoMap['b2in'][$type]; @@ -48,6 +66,7 @@ class ThemeHelper public static function getFaviconPath(): string { $theme = config('app.theme', 'b2in'); + return "img/favicons/{$theme}-favicon.ico"; } @@ -57,6 +76,7 @@ class ThemeHelper public static function getThemeCssPath(): string { $theme = config('app.theme', 'b2in'); + return "resources/css/web/theme-{$theme}.css"; } @@ -66,6 +86,7 @@ class ThemeHelper public static function getDomainConfig(): array { $theme = config('app.theme', 'b2in'); + return config("domains.domains.{$theme}", []); } @@ -75,6 +96,7 @@ class ThemeHelper public static function getPrimaryColor(): string { $config = self::getDomainConfig(); + return $config['color_scheme']['primary'] ?? '#2b3f51'; } @@ -84,6 +106,7 @@ class ThemeHelper public static function getSecondaryColor(): string { $config = self::getDomainConfig(); + return $config['color_scheme']['secondary'] ?? '#20a0da'; } @@ -93,6 +116,7 @@ class ThemeHelper public static function getPrimaryFont(): string { $config = self::getDomainConfig(); + return $config['font_family']['primary'] ?? 'Inter'; } @@ -102,6 +126,7 @@ class ThemeHelper public static function getSecondaryFont(): string { $config = self::getDomainConfig(); + return $config['font_family']['secondary'] ?? 'IBM Plex Sans'; } @@ -111,12 +136,14 @@ class ThemeHelper public static function getDomainName(): string { $config = self::getDomainConfig(); + return $config['domain_name'] ?? 'B2IN'; } public static function getDomainUrl(): string { $config = self::getDomainConfig(); + return $config['url'] ?? config('app.url'); } @@ -127,7 +154,7 @@ class ThemeHelper { $brand = $brand ?? config('app.theme', 'b2in'); $config = config("domains.domains.{$brand}", []); - + return [ 'primary' => $config['color_scheme']['primary'] ?? '#2b3f51', 'secondary' => $config['color_scheme']['secondary'] ?? '#20a0da', @@ -141,14 +168,14 @@ class ThemeHelper public static function getBrandName(?string $brand = null): string { $brand = $brand ?? config('app.theme', 'b2in'); - + $brandNames = [ 'b2in' => 'B2IN', 'b2a' => 'B2A', 'stileigentum' => 'StilEigentum', 'style2own' => 'Style2Own', ]; - + return $brandNames[$brand] ?? strtoupper($brand); } @@ -158,6 +185,7 @@ class ThemeHelper public static function getBrandConfig(?string $brand = null): array { $brand = $brand ?? config('app.theme', 'b2in'); + return config("domains.domains.{$brand}", []); } } diff --git a/app/Http/Controllers/DisplayEmbedController.php b/app/Http/Controllers/DisplayEmbedController.php new file mode 100644 index 00000000..1216c724 --- /dev/null +++ b/app/Http/Controllers/DisplayEmbedController.php @@ -0,0 +1,34 @@ +is_active, 404); + + $playlist = $display->livePlaylist()->withCount('modules')->first(); + + abort_if($playlist === null || $playlist->modules_count === 0, 404); + + // Der Player liest die ID aus dem Pfad (/display/{id}) und holt sich die Konfiguration + // über die öffentliche Route /api/display/{display}/config – dieselbe Quelle wie das + // physische Display. Keine doppelte Pflege. + return response()->file(public_path('_cabinet/display/index.html'), [ + 'Cache-Control' => 'no-cache, must-revalidate', + 'X-Robots-Tag' => 'noindex', + ]); + } +} diff --git a/app/Livewire/Admin/Cms/CabinetInfoTablet.php b/app/Livewire/Admin/Cms/CabinetInfoTablet.php index 79711375..f9bb1828 100644 --- a/app/Livewire/Admin/Cms/CabinetInfoTablet.php +++ b/app/Livewire/Admin/Cms/CabinetInfoTablet.php @@ -98,13 +98,35 @@ class CabinetInfoTablet extends Component } /** - * @param array $hours Optional: Time picker values from DOM (bypasses wire:model sync issues) + * Zeit-Properties, die beim Speichern aus dem DOM uebernommen werden duerfen. + * + * @return array + */ + private function hourProperties(): array + { + return [ + 'hoursMondayOpen', 'hoursMondayClose', + 'hoursTuesdayOpen', 'hoursTuesdayClose', + 'hoursWednesdayOpen', 'hoursWednesdayClose', + 'hoursThursdayOpen', 'hoursThursdayClose', + 'hoursFridayOpen', 'hoursFridayClose', + 'hoursSaturdayOpen', 'hoursSaturdayClose', + 'hoursSundayOpen', 'hoursSundayClose', + ]; + } + + /** + * Der Flux-Zeit-Picker meldet das Leeren per X nicht zuverlaessig an wire:model. + * Deshalb liest das Formular die Werte beim Absenden aus dem DOM und uebergibt sie hier. + * Nur die Zeitfelder duerfen so gesetzt werden – alles andere kommt aus wire:model. + * + * @param array $hours Zeit-Picker-Werte aus dem DOM */ public function save(array $hours = []): void { - foreach ($hours as $prop => $value) { - if (property_exists($this, $prop)) { - $this->{$prop} = $value ?? ''; + foreach ($this->hourProperties() as $prop) { + if (array_key_exists($prop, $hours)) { + $this->{$prop} = is_string($hours[$prop]) ? $hours[$prop] : ''; } } diff --git a/app/Livewire/Admin/Cms/DisplayVersionEditor.php b/app/Livewire/Admin/Cms/DisplayVersionEditor.php index ec85b24a..b990f1dd 100644 --- a/app/Livewire/Admin/Cms/DisplayVersionEditor.php +++ b/app/Livewire/Admin/Cms/DisplayVersionEditor.php @@ -53,7 +53,8 @@ class DisplayVersionEditor extends Component public string $mediaSubline = ''; - public int $mediaDuration = 10; + /** Anzeigedauer eines Bildes in Sekunden als Eingabetext, Komma oder Punkt erlaubt („1,5“). */ + public string $mediaDuration = '10'; public bool $mediaIsActive = true; @@ -247,6 +248,12 @@ class DisplayVersionEditor extends Component public function saveItem(): void { + if ($this->itemType === 'media' && $this->mediaType === 'image' && self::parseDuration($this->mediaDuration) === null) { + $this->addError('mediaDuration', 'Bitte eine Dauer zwischen 0,5 und 120 Sekunden angeben, z. B. 1,5.'); + + return; + } + $content = $this->buildItemContent(); $isActive = $this->getActiveFlag(); @@ -279,6 +286,41 @@ class DisplayVersionEditor extends Component $this->refreshModulePreview(); } + /** + * Sekunden aus der Eingabe lesen: „1,5“, „1.5“ und „10“ sind gültig, gerundet auf Zehntel. + * Erlaubt sind 0,5 bis 120 Sekunden; alles andere ergibt null. + */ + public static function parseDuration(string|int|float $input): ?float + { + $normalized = str_replace(',', '.', trim((string) $input)); + + if ($normalized === '' || ! is_numeric($normalized)) { + return null; + } + + $seconds = round((float) $normalized, 1); + + if ($seconds < 0.5 || $seconds > 120) { + return null; + } + + return $seconds; + } + + /** + * Sekunden für die Anzeige: ganze Zahlen ohne Nachkommastelle, sonst mit Komma („1,5“). + */ + public static function formatDuration(int|float|string|null $seconds): string + { + $seconds = (float) str_replace(',', '.', (string) ($seconds ?? 10)); + + if (floor($seconds) === $seconds) { + return (string) (int) $seconds; + } + + return str_replace('.', ',', (string) round($seconds, 1)); + } + public function deleteItem(int $id): void { DisplayVersionItem::findOrFail($id)->delete(); @@ -437,7 +479,7 @@ class DisplayVersionEditor extends Component $this->mediaUrl = $content['media_url'] ?? ''; $this->mediaHeadline = $content['headline'] ?? ''; $this->mediaSubline = $content['subline'] ?? ''; - $this->mediaDuration = $content['duration_seconds'] ?? 10; + $this->mediaDuration = self::formatDuration($content['duration_seconds'] ?? 10); $this->mediaIsActive = $isActive; } @@ -501,7 +543,7 @@ class DisplayVersionEditor extends Component 'media_url' => $this->mediaUrl, 'headline' => $this->mediaHeadline, 'subline' => $this->mediaSubline, - 'duration_seconds' => $this->mediaDuration, + 'duration_seconds' => self::parseDuration($this->mediaDuration) ?? 10, ], 'slide' => [ 'type' => 'detail', @@ -663,7 +705,7 @@ class DisplayVersionEditor extends Component $this->mediaUrl = ''; $this->mediaHeadline = ''; $this->mediaSubline = ''; - $this->mediaDuration = 10; + $this->mediaDuration = '10'; $this->mediaIsActive = true; $this->slideShowLogo = true; $this->slideLogoUrl = ''; diff --git a/app/Livewire/Web/Components/Sections/AboutHero.php b/app/Livewire/Web/Components/Sections/AboutHero.php index c0aaa1a7..1cb3233a 100644 --- a/app/Livewire/Web/Components/Sections/AboutHero.php +++ b/app/Livewire/Web/Components/Sections/AboutHero.php @@ -8,9 +8,13 @@ class AboutHero extends Component { public $content = []; - public function mount() + /** Sektionsschluessel – die Immobilien-Welt nutzt eigene Inhalte (immobilien_about_hero). */ + public string $section = 'about_hero'; + + public function mount(string $section = 'about_hero') { - $this->content = cms_theme_section('about_hero'); + $this->section = $section; + $this->content = cms_theme_section($this->section); } public function render() diff --git a/app/Livewire/Web/Components/Sections/AudienceSwitch.php b/app/Livewire/Web/Components/Sections/AudienceSwitch.php new file mode 100644 index 00000000..83eea87c --- /dev/null +++ b/app/Livewire/Web/Components/Sections/AudienceSwitch.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('audience_switch'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.audience-switch'); + } +} diff --git a/app/Livewire/Web/Components/Sections/BrandDetail.php b/app/Livewire/Web/Components/Sections/BrandDetail.php new file mode 100644 index 00000000..6f613136 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/BrandDetail.php @@ -0,0 +1,89 @@ + */ + public array $brand = []; + + /** @var array> */ + public array $otherBrands = []; + + /** + * Galerie getrennt nach realisierten Projekten und Inspirationen. + * + * @var array{projects: array>, inspirations: array>} + */ + public array $gallery = ['projects' => [], 'inspirations' => []]; + + public function mount(string $slug): void + { + $brands = CmsBrand::forFrontend(); + + abort_unless(isset($brands[$slug]), 404); + + $this->slug = $slug; + $this->brand = $brands[$slug]; + $this->gallery = CmsBrand::groupGallery($this->brand['gallery'] ?? []); + $this->otherBrands = collect($brands) + ->reject(fn (array $brand, string $key) => $key === $slug) + ->values() + ->all(); + + $craft = $this->craftSection($slug, $brands); + + if ($craft !== null) { + $this->brand['sections'][] = $craft; + } + } + + /** + * Gemeinsamer Handwerk-Abschluss (Lang-Sektion `brand_craft`, Nachtrag 15): steht als letzter + * Abschnitt auf jeder Markenseite – außer auf der Seite der Handwerksmarke selbst. Der Link + * dorthin erscheint nur, wenn diese Marke veröffentlicht ist und eine Detailseite hat. + * + * @param array> $brands + * @return array|null + */ + private function craftSection(string $slug, array $brands): ?array + { + $content = cms_theme_section('brand_craft'); + + if (! is_array($content)) { + return null; + } + + $craftSlug = (string) ($content['brand_slug'] ?? ''); + + if ($craftSlug !== '' && $craftSlug === $slug) { + return null; + } + + $sections = CmsBrand::normalizeSections([$content]); + + if ($sections === []) { + return null; + } + + $section = $sections[0]; + $section['anchor'] = 'handwerk'; + + if ($craftSlug !== '' && ! empty($brands[$craftSlug]['has_detail_page'])) { + $section['link'] = route('marken.show', ['slug' => $craftSlug]); + $section['link_text'] = (string) ($content['link_text'] ?? $brands[$craftSlug]['name'] ?? ''); + } + + return $section; + } + + public function render() + { + return view('livewire.web.components.sections.brand-detail'); + } +} diff --git a/app/Livewire/Web/Components/Sections/BrandOverview.php b/app/Livewire/Web/Components/Sections/BrandOverview.php new file mode 100644 index 00000000..a810fe5d --- /dev/null +++ b/app/Livewire/Web/Components/Sections/BrandOverview.php @@ -0,0 +1,25 @@ +> */ + public array $brands = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $this->brands = array_values(CmsBrand::forFrontend()); + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.brand-overview'); + } +} diff --git a/app/Livewire/Web/Components/Sections/CTASection.php b/app/Livewire/Web/Components/Sections/CTASection.php index 6fc5c452..b668225b 100644 --- a/app/Livewire/Web/Components/Sections/CTASection.php +++ b/app/Livewire/Web/Components/Sections/CTASection.php @@ -12,8 +12,13 @@ class CTASection extends Component public $section = ''; - public function mount($bg = 'bg-secondary', $section = 'cta_section') + public function mount($bg = 'cta-band', $section = 'cta_section') { + // Der Standard-Abschluss hat je Welt eigene Texte und fuehrt zur Kontaktseite der Welt. + if ($section === 'cta_section' && b2in_world() === 'immobilien') { + $section = 'cta_section_immobilien'; + } + $this->section = $section; $this->content = cms_theme_section($this->section); $this->bg = $bg; diff --git a/app/Livewire/Web/Components/Sections/CoreBrands.php b/app/Livewire/Web/Components/Sections/CoreBrands.php new file mode 100644 index 00000000..521748f8 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/CoreBrands.php @@ -0,0 +1,30 @@ + */ + public array $content = []; + + /** @var array> */ + public array $brands = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('core_brands'); + $this->content = is_array($section) ? $section : []; + $this->brands = array_values(CmsBrand::forFrontend()); + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.core-brands'); + } +} diff --git a/app/Livewire/Web/Components/Sections/DisplayFeed.php b/app/Livewire/Web/Components/Sections/DisplayFeed.php new file mode 100644 index 00000000..2f70581d --- /dev/null +++ b/app/Livewire/Web/Components/Sections/DisplayFeed.php @@ -0,0 +1,72 @@ + */ + public array $content = []; + + public ?int $displayId = null; + + public string $bg = ''; + + public bool $showLink = true; + + public function mount(string $bg = '', bool $showLink = true): void + { + $section = cms_theme_section('display_feed'); + $content = is_array($section) ? $section : []; + + $this->bg = $bg; + $this->showLink = $showLink; + $this->displayId = $this->resolveDisplayId($content); + + // Ohne Display bleiben auch die Texte leer – sie hätten sonst nichts zu beschriften und + // stünden trotzdem im Livewire-Snapshot der Seite. + $this->content = $this->displayId === null ? [] : $content; + } + + /** + * Im CMS hinterlegtes Display, sonst das erste aktive mit veröffentlichter Playlist. + * + * Ohne passendes Display rendert die Sektion nichts – lieber keine Sektion als ein + * Rahmen, in dem sich ewig ein Ladebalken dreht. + * + * @param array $content + */ + private function resolveDisplayId(array $content): ?int + { + $configured = $content['display_id'] ?? null; + + $query = Display::query() + ->where('is_active', true) + ->whereHas('playlists', fn ($q) => $q->where('status', DisplayPlaylist::STATUS_PUBLISHED) + ->whereHas('modules')); + + if (is_numeric($configured) && (int) $configured > 0) { + $display = (clone $query)->whereKey((int) $configured)->first(); + + if ($display !== null) { + return $display->id; + } + } + + return $query->orderBy('id')->value('id'); + } + + public function render() + { + return view('livewire.web.components.sections.display-feed'); + } +} diff --git a/app/Livewire/Web/Components/Sections/HeroSlider.php b/app/Livewire/Web/Components/Sections/HeroSlider.php index 738e0de2..ec27e292 100644 --- a/app/Livewire/Web/Components/Sections/HeroSlider.php +++ b/app/Livewire/Web/Components/Sections/HeroSlider.php @@ -2,42 +2,96 @@ 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. + */ class HeroSlider extends Component { - public $content = []; + /** @var array */ + public array $content = []; - public $currentSlide = 0; + /** @var array */ + public array $slides = []; - public function mount() + public function mount(): void { - $this->content = cms_theme_section('hero_slider'); + $section = cms_theme_section('hero_slider'); + $this->content = is_array($section) ? $section : []; + $this->slides = $this->slidesFromContent() ?: $this->slidesFromBrands(); } - public function nextSlide() + /** + * @return array + */ + private function slidesFromContent(): array { - if (! empty($this->content['slides'])) { - $this->currentSlide = ($this->currentSlide + 1) % count($this->content['slides']); + $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'] ?? '')), + ]; } + + return $slides; } - public function previousSlide() + /** + * @return array + */ + private function slidesFromBrands(): array { - if (! empty($this->content['slides'])) { - $totalSlides = count($this->content['slides']); - $this->currentSlide = ($this->currentSlide - 1 + $totalSlides) % $totalSlides; + $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'] ?? '')), + ]; } + + return $slides; } - public function setSlide($index) + public function getIntervalMsProperty(): int { - if (! empty($this->content['slides']) && $index >= 0 && $index < count($this->content['slides'])) { - $this->currentSlide = $index; - } + $seconds = (float) ($this->content['interval'] ?? 7); + + return (int) round(max(3, min(30, $seconds)) * 1000); } - public function render() + public function render(): \Illuminate\View\View { return view('livewire.web.components.sections.hero-slider'); } diff --git a/app/Livewire/Web/Components/Sections/ImmobilienTeaser.php b/app/Livewire/Web/Components/Sections/ImmobilienTeaser.php new file mode 100644 index 00000000..4e454178 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/ImmobilienTeaser.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('immobilien_teaser'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.immobilien-teaser'); + } +} diff --git a/app/Livewire/Web/Components/Sections/MagazinDetail.php b/app/Livewire/Web/Components/Sections/MagazinDetail.php index 0ef295b5..1a1fe7a3 100644 --- a/app/Livewire/Web/Components/Sections/MagazinDetail.php +++ b/app/Livewire/Web/Components/Sections/MagazinDetail.php @@ -2,11 +2,12 @@ namespace App\Livewire\Web\Components\Sections; +use App\Models\CmsArticle; use Livewire\Component; class MagazinDetail extends Component { - public $articleId; + public $slug; public $article; @@ -14,25 +15,41 @@ class MagazinDetail extends Component public $content; - public function mount($id = 1) + /** Rueckweg zur Magazinliste der Welt, zu der der Beitrag gehoert. */ + public string $listUrl = ''; + + public function mount($slug) { - $this->articleId = (int) $id; + $this->slug = (string) $slug; $this->loadArticle(); + $this->listUrl = $this->articleWorld() === CmsArticle::WORLD_IMMOBILIEN + ? route('immobilien.magazin') + : route('magazin'); $this->loadRelatedArticles(); $this->loadThemeContent(); } + private function articleWorld(): string + { + return (string) ($this->article['world'] ?? CmsArticle::WORLD_INTERIOR); + } + private function loadArticle() { $articles = $this->getArticlesData(); - $this->article = $articles[$this->articleId] ?? $articles[1]; + + // Unbekannte Slugs haben frueher stillschweigend Artikel 1 ausgeliefert – mit Status 200. + // Das erzeugt Duplicate Content unter beliebig vielen URLs. + abort_unless(isset($articles[$this->slug]), 404); + + $this->article = $articles[$this->slug]; } private function loadRelatedArticles() { - $articles = $this->getArticlesData(); - $this->relatedArticles = collect($articles) - ->filter(fn ($article, $key) => $key != $this->articleId) + $this->relatedArticles = collect($this->getArticlesData()) + ->filter(fn (array $article) => ($article['world'] ?? CmsArticle::WORLD_INTERIOR) === $this->articleWorld()) + ->reject(fn (array $article, string $slug) => $slug === $this->slug) ->take(2) ->values() ->toArray(); @@ -43,9 +60,18 @@ class MagazinDetail extends Component $this->content = cms_theme_section('magazin_detail'); } - private function getArticlesData() + /** @var array>|null */ + private ?array $articlesCache = null; + + /** + * Wird beim Rendern zweimal gebraucht (Artikel + verwandte Artikel) – deshalb gemerkt, + * damit die Detailseite nur eine Abfrage auslöst. + * + * @return array> + */ + private function getArticlesData(): array { - return trans('b2in.articles'); + return $this->articlesCache ??= CmsArticle::forFrontend(); } public function render() diff --git a/app/Livewire/Web/Components/Sections/MagazinList.php b/app/Livewire/Web/Components/Sections/MagazinList.php index ecd1e800..039a63e2 100644 --- a/app/Livewire/Web/Components/Sections/MagazinList.php +++ b/app/Livewire/Web/Components/Sections/MagazinList.php @@ -2,6 +2,7 @@ namespace App\Livewire\Web\Components\Sections; +use App\Models\CmsArticle; use Livewire\Component; class MagazinList extends Component @@ -10,34 +11,41 @@ class MagazinList extends Component public $content = []; - public function mount() + /** Farbwelt der Liste: `interior` (/magazin) oder `immobilien` (/immobilien/magazin). */ + public string $world = CmsArticle::WORLD_INTERIOR; + + public function mount(string $world = CmsArticle::WORLD_INTERIOR) { + $this->world = in_array($world, CmsArticle::WORLDS, true) ? $world : CmsArticle::WORLD_INTERIOR; $this->loadPosts(); $this->loadThemeContent(); } private function loadPosts() { - $articles = trans('b2in.articles'); - $this->posts = collect($articles)->map(function ($article) { + $this->posts = collect(CmsArticle::forFrontend($this->world))->map(function (array $article) { return [ - 'id' => $article['id'], + 'slug' => $article['slug'], 'title' => $article['title'], - 'excerpt' => $article['content']['intro'], // Using intro as excerpt + 'excerpt' => $article['content']['intro'] ?? '', 'image' => $article['image'], 'date' => $article['date'], 'readTime' => $article['readTime'], ]; - })->all(); + })->values()->all(); } private function loadThemeContent() { - $this->content = cms_theme_section('magazin_list'); + $this->content = cms_theme_section( + $this->world === CmsArticle::WORLD_IMMOBILIEN ? 'magazin_list_immobilien' : 'magazin_list' + ); } public function render() { - return view('livewire.web.components.sections.magazin-list'); + return view('livewire.web.components.sections.magazin-list', [ + 'posts' => $this->posts, + ]); } } diff --git a/app/Livewire/Web/Components/Sections/MarkenCta.php b/app/Livewire/Web/Components/Sections/MarkenCta.php new file mode 100644 index 00000000..07af19f2 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/MarkenCta.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('marken_cta'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.marken-cta'); + } +} diff --git a/app/Livewire/Web/Components/Sections/MarkenGrowth.php b/app/Livewire/Web/Components/Sections/MarkenGrowth.php new file mode 100644 index 00000000..e5820af1 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/MarkenGrowth.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('marken_growth'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.marken-growth'); + } +} diff --git a/app/Livewire/Web/Components/Sections/MarkenNetwork.php b/app/Livewire/Web/Components/Sections/MarkenNetwork.php new file mode 100644 index 00000000..971faf80 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/MarkenNetwork.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('marken_network'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.marken-network'); + } +} diff --git a/app/Livewire/Web/Components/Sections/OriginSection.php b/app/Livewire/Web/Components/Sections/OriginSection.php new file mode 100644 index 00000000..9705013a --- /dev/null +++ b/app/Livewire/Web/Components/Sections/OriginSection.php @@ -0,0 +1,83 @@ + + */ + public const MAP_POSITIONS = [ + 'rotpunkt' => ['x' => 330, 'y' => 55, 'label' => 'left'], + 'februe' => ['x' => 480, 'y' => 90, 'label' => 'right'], + 'tischlerei' => ['x' => 295, 'y' => 285, 'label' => 'left'], + 'cabinet' => ['x' => 80, 'y' => 345, 'label' => 'right'], + ]; + + /** @var array */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('origin_section'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + /** + * Veröffentlichte Marken mit gepflegtem Fertigungsort, in der Reihenfolge der Marken. + * + * Eine unveröffentlichte Marke fällt heraus, eine Marke ohne Ort ebenfalls. Der Link zur + * Markenseite hängt daran, ob die Marke eine Detailseite hat (Lang-Fallback: keine). + * + * @return list + */ + public function getLocationsProperty(): array + { + $out = []; + + foreach (CmsBrand::forFrontend() as $slug => $brand) { + $city = trim((string) ($brand['origin_city'] ?? '')); + $name = trim((string) ($brand['name'] ?? '')); + + if ($city === '' || $name === '') { + continue; + } + + $out[] = [ + 'slug' => (string) $slug, + 'city' => $city, + 'brand' => $name, + 'role' => trim((string) ($brand['origin_role'] ?? '')), + 'distance' => trim((string) ($brand['origin_distance'] ?? '')), + 'link' => ! empty($brand['has_detail_page']) ? '/marken/'.$slug : null, + 'map' => self::MAP_POSITIONS[$slug] ?? null, + ]; + } + + return $out; + } + + public function render() + { + return view('livewire.web.components.sections.origin-section'); + } +} diff --git a/app/Livewire/Web/Components/Sections/OurStory.php b/app/Livewire/Web/Components/Sections/OurStory.php index 77926785..5086f981 100644 --- a/app/Livewire/Web/Components/Sections/OurStory.php +++ b/app/Livewire/Web/Components/Sections/OurStory.php @@ -8,9 +8,13 @@ class OurStory extends Component { public $content = []; - public function mount() + /** Sektionsschluessel – die Immobilien-Welt nutzt eigene Inhalte (immobilien_our_story). */ + public string $section = 'our_story'; + + public function mount(string $section = 'our_story') { - $this->content = cms_theme_section('our_story'); + $this->section = $section; + $this->content = cms_theme_section($this->section); } public function render() diff --git a/app/Livewire/Web/Components/Sections/OurValues.php b/app/Livewire/Web/Components/Sections/OurValues.php index d6979b66..f13ddabd 100644 --- a/app/Livewire/Web/Components/Sections/OurValues.php +++ b/app/Livewire/Web/Components/Sections/OurValues.php @@ -8,9 +8,13 @@ class OurValues extends Component { public $content = []; - public function mount() + /** Sektionsschluessel – die Immobilien-Welt nutzt eigene Inhalte (immobilien_our_values). */ + public string $section = 'our_values'; + + public function mount(string $section = 'our_values') { - $this->content = cms_theme_section('our_values'); + $this->section = $section; + $this->content = cms_theme_section($this->section); } public function render() diff --git a/app/Livewire/Web/Components/Sections/PageHero.php b/app/Livewire/Web/Components/Sections/PageHero.php new file mode 100644 index 00000000..71094925 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/PageHero.php @@ -0,0 +1,34 @@ + */ + public array $content = []; + + public string $bg = ''; + + public string $breadcrumb = ''; + + public function mount(string $section = 'marken_hero', string $breadcrumb = '', string $bg = ''): void + { + $content = cms_theme_section($section); + $this->content = is_array($content) ? $content : []; + $this->breadcrumb = $breadcrumb !== '' ? $breadcrumb : __('ui.brands'); + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.page-hero'); + } +} diff --git a/app/Livewire/Web/Components/Sections/ReferenceProjects.php b/app/Livewire/Web/Components/Sections/ReferenceProjects.php new file mode 100644 index 00000000..7e91df8c --- /dev/null +++ b/app/Livewire/Web/Components/Sections/ReferenceProjects.php @@ -0,0 +1,113 @@ + */ + public array $content = []; + + /** @var array> */ + public array $projects = []; + + public string $bg = ''; + + public int $limit = 0; + + public function mount(string $bg = '', int $limit = 0): void + { + $section = cms_theme_section('reference_projects'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + $this->limit = $limit; + + $projects = $this->fromReferences(); + + if ($projects === []) { + $projects = $this->fromBrandGalleries(); + } + + $this->projects = $this->limit > 0 ? array_slice($projects, 0, $this->limit) : $projects; + } + + /** + * @return array> + */ + private function fromReferences(): array + { + $references = CmsReference::query()->published()->ordered()->get(); + + if ($references->isEmpty()) { + return []; + } + + $brands = CmsBrand::forFrontend(); + $projects = []; + + foreach ($references as $reference) { + $gallery = CmsReference::normalizeGallery($reference->gallery); + $image = (string) ($reference->image ?: ($gallery[0]['image'] ?? '')); + + if ($image === '') { + continue; + } + + $projects[] = [ + 'key' => 'ref-'.$reference->id, + 'image' => $image, + 'caption' => (string) $reference->title, + 'meta' => implode(' · ', array_filter([$reference->location, $reference->year])), + 'description' => (string) $reference->description, + 'gallery' => array_values(array_filter($gallery, fn (array $item) => $item['image'] !== $image)), + 'brands' => $reference->resolveBrands($brands), + ]; + } + + return $projects; + } + + /** + * @return array> + */ + private function fromBrandGalleries(): array + { + $projects = []; + + foreach (CmsBrand::forFrontend() as $slug => $brand) { + foreach (CmsBrand::groupGallery($brand['gallery'] ?? [])['projects'] as $index => $item) { + $projects[] = [ + 'key' => $slug.'-'.$index, + 'image' => $item['image'], + 'caption' => $item['caption'], + 'meta' => '', + 'description' => '', + 'gallery' => [], + 'brands' => [[ + 'slug' => $slug, + 'name' => (string) ($brand['name'] ?? ''), + 'link' => ! empty($brand['has_detail_page']) ? '/marken/'.$slug : null, + ]], + ]; + } + } + + return $projects; + } + + public function render() + { + return view('livewire.web.components.sections.reference-projects'); + } +} diff --git a/app/Livewire/Web/Components/Sections/Sponsoring.php b/app/Livewire/Web/Components/Sections/Sponsoring.php new file mode 100644 index 00000000..5e333900 --- /dev/null +++ b/app/Livewire/Web/Components/Sections/Sponsoring.php @@ -0,0 +1,25 @@ + */ + public array $content = []; + + public string $bg = ''; + + public function mount(string $bg = ''): void + { + $section = cms_theme_section('sponsoring'); + $this->content = is_array($section) ? $section : []; + $this->bg = $bg; + } + + public function render() + { + return view('livewire.web.components.sections.sponsoring'); + } +} diff --git a/app/Livewire/Web/Components/UI/AnnouncementBar.php b/app/Livewire/Web/Components/UI/AnnouncementBar.php index 610b0ca8..b511e779 100644 --- a/app/Livewire/Web/Components/UI/AnnouncementBar.php +++ b/app/Livewire/Web/Components/UI/AnnouncementBar.php @@ -8,9 +8,35 @@ class AnnouncementBar extends Component { public array $content = []; + /** Farbwelt der Seite – jede Welt hat ihre eigene Meldung. */ + public string $world = 'interior'; + public function mount(): void { - $this->content = cms_theme_section('announcement_bar'); + $this->world = b2in_world(); + + // Interior: global.announcement_bar, Immobilien: global.announcement_bar_immobilien. + $section = cms_theme_section($this->world === 'immobilien' ? 'announcement_bar_immobilien' : 'announcement_bar'); + $this->content = is_array($section) ? $section : []; + } + + /** + * Schluessel, unter dem sich der Browser das Wegklicken merkt. + * + * Leitet sich aus dem Inhalt ab, nicht nur aus der gepflegten `id`: Wird die Leiste im CMS + * neu belegt, ohne die `id` zu aendern, bekaemen Wiederkehrer die neue Meldung sonst nie + * zu sehen, weil ihr alter Dismiss-Eintrag im localStorage weitergilt. + */ + public function getDismissKeyProperty(): string + { + $fingerprint = implode('|', [ + $this->content['id'] ?? 'default', + $this->content['badge'] ?? '', + $this->content['text'] ?? '', + $this->content['link_url'] ?? '', + ]); + + return 'announcement_dismissed_'.substr(md5($fingerprint), 0, 12); } public function render() diff --git a/app/Livewire/Web/Components/UI/AppointmentWidget.php b/app/Livewire/Web/Components/UI/AppointmentWidget.php new file mode 100644 index 00000000..a6362f19 --- /dev/null +++ b/app/Livewire/Web/Components/UI/AppointmentWidget.php @@ -0,0 +1,166 @@ + 'Projektausstattung', + 'immobilien' => 'Immobilien', + 'sonstiges' => 'Sonstiges', + ]; + + /** Farbwelt der Seite – in der Immobilien-Welt ist das Anliegen vorbelegt. */ + public string $world = 'interior'; + + public string $name = ''; + + public string $email = ''; + + public string $phone = ''; + + public string $topic = ''; + + public string $preferredDate = ''; + + public string $message = ''; + + public bool $privacy = false; + + public bool $success = false; + + /** @var string Honeypot – muss leer bleiben. */ + public string $website = ''; + + public ?int $formLoadedAt = null; + + /** @var array */ + public array $content = []; + + public function mount(): void + { + $section = cms_theme_section('appointment_widget'); + $this->content = is_array($section) ? $section : []; + $this->formLoadedAt = time(); + $this->world = b2in_world(); + + if ($this->world === 'immobilien') { + $this->topic = 'immobilien'; + } + + $this->applyBrandContext(); + } + + /** + * Auf einer Markenseite trägt der Button die Beschriftung dieser Marke. + * + * Die Zulieferung sieht ausdrücklich keinen zweiten CTA-Button vor, sondern eine + * kontextbezogene Beschriftung des vorhandenen Widgets. Da das Widget im Layout steckt und + * nicht je Seite eingebunden wird, liest es den Kontext aus der Route. + */ + private function applyBrandContext(): void + { + $route = request()->route(); + + if ($route?->getName() !== 'marken.show') { + return; + } + + $slug = (string) $route->parameter('slug'); + $brand = CmsBrand::forFrontend()[$slug] ?? null; + $label = is_array($brand) ? trim((string) ($brand['widget_label'] ?? '')) : ''; + + if ($label !== '') { + $this->content['button_label'] = $label; + } + } + + /** + * Auswahl „Anliegen": gepflegte Kernmarken zuerst, danach die festen Einträge. + * + * @return array + */ + public function getTopicsProperty(): array + { + $brands = []; + + foreach (CmsBrand::forFrontend() as $slug => $brand) { + $brands[$slug] = (string) ($brand['name'] ?? $slug); + } + + return $brands + self::EXTRA_TOPICS; + } + + public function getWhatsappUrlProperty(): string + { + return whatsapp_link(); + } + + public function submit(ContactFormService $service): void + { + $this->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email:rfc', 'max:255'], + 'phone' => ['nullable', 'string', 'max:80'], + 'topic' => ['nullable', 'string', 'max:255'], + 'preferredDate' => ['nullable', 'date', 'after_or_equal:today'], + 'message' => ['nullable', 'string', 'max:2000'], + 'privacy' => ['accepted'], + 'website' => ['nullable', 'string', 'max:0'], + ], [ + 'preferredDate.after_or_equal' => __('ui.appointment.date_in_past'), + ]); + + $topicLabel = $this->topics[$this->topic] ?? ''; + + $payload = [ + 'name' => $this->name, + 'email' => $this->email, + 'phone' => $this->phone, + 'topic' => $topicLabel, + 'preferred_date' => $this->preferredDate, + 'message' => $this->message, + 'website' => $this->website, + 'source' => request()->path(), + 'ip' => request()->ip(), + 'user_agent' => request()->userAgent(), + 'is_spam' => false, + ]; + + $payload['is_spam'] = SpamDetector::fromConfig()->detect($payload, $this->formLoadedAt); + + $subject = ($this->world === 'immobilien' ? 'Terminanfrage B2in Immobilien' : 'Terminanfrage B2in Interior') + .($topicLabel !== '' ? ': '.$topicLabel : ''); + $service->handle($payload, $subject, 'appointment-widget'); + + $this->success = true; + $this->reset(['name', 'email', 'phone', 'topic', 'preferredDate', 'message', 'privacy', 'website']); + $this->topic = $this->world === 'immobilien' ? 'immobilien' : ''; + $this->formLoadedAt = time(); + } + + public function startOver(): void + { + $this->success = false; + $this->formLoadedAt = time(); + } + + public function render(): \Illuminate\View\View + { + return view('livewire.web.components.ui.appointment-widget'); + } +} diff --git a/app/Livewire/Web/Components/UI/ContactForm.php b/app/Livewire/Web/Components/UI/ContactForm.php index b1172057..46b0df84 100644 --- a/app/Livewire/Web/Components/UI/ContactForm.php +++ b/app/Livewire/Web/Components/UI/ContactForm.php @@ -4,6 +4,8 @@ namespace App\Livewire\Web\Components\Ui; use Acme\ContactForm\ContactFormService; use Acme\ContactForm\SpamDetector; +use App\Models\CmsBrand; +use Illuminate\Validation\Rule; use Livewire\Component; class ContactForm extends Component @@ -34,16 +36,109 @@ class ContactForm extends Component /** @var array */ public array $content = []; - public function mount(): void + /** Farbwelt der Seite (`interior` | `immobilien`). */ + public string $world = 'interior'; + + public function mount(string $world = 'interior'): void { - $this->content = cms_theme_section('contact_form'); + $this->world = $world === 'immobilien' ? 'immobilien' : 'interior'; + $this->content = $this->loadContent(); $this->formLoadedAt = time(); } - /** @return array */ + /** + * Inhalte der Welt. Die Immobilien-Welt liegt als Overlay über der Interior-Sektion: + * `contact_form_immobilien` trägt nur, was abweicht (Einstieg, Betreffe) – Adresse, + * Beschriftungen und Social Media erbt sie von `contact_form`. + * + * @return array + */ + private function loadContent(): array + { + $base = cms_theme_section('contact_form'); + $base = is_array($base) ? $base : []; + + if ($this->world !== 'immobilien') { + return $base; + } + + $overlay = cms_theme_section('contact_form_immobilien'); + + if (! is_array($overlay)) { + return $base; + } + + return array_replace_recursive($base, $overlay); + } + + /** + * Betreffe in Gruppen: Interior (mit den gepflegten Kernmarken vorneweg), Immobilien, B2in. + * Die Welt der Seite bestimmt die Reihenfolge – ihre Gruppe steht oben. + * + * Ohne `subject_groups` (aeltere CMS-Staende) faellt das Formular auf die flache Liste zurueck. + * + * @return array}> + */ + public function getSubjectGroupsProperty(): array + { + $groups = $this->content['form']['subject_groups'] ?? null; + + if (! is_array($groups) || $groups === []) { + $flat = $this->content['form']['subjects'] ?? []; + unset($flat['']); + + return $flat === [] ? [] : ['all' => ['label' => '', 'subjects' => $flat]]; + } + + $brands = []; + + foreach (CmsBrand::forFrontend() as $slug => $brand) { + $brands[$slug] = (string) ($brand['name'] ?? $slug); + } + + $out = []; + + foreach ($groups as $key => $group) { + $subjects = is_array($group['subjects'] ?? null) ? $group['subjects'] : []; + + if ($key === 'interior') { + $subjects = $brands + $subjects; + } + + $out[$key] = [ + 'label' => (string) ($group['label'] ?? ''), + 'subjects' => $subjects, + ]; + } + + $first = $this->world === 'immobilien' ? 'immobilien' : 'interior'; + + if (isset($out[$first])) { + $out = [$first => $out[$first]] + $out; + } + + return $out; + } + + /** + * Alle waehlbaren Betreffe als flache Liste (Schluessel → Beschriftung). + * + * @return array + */ public function getSubjectsProperty(): array { - return $this->content['form']['subjects'] ?? []; + $flat = []; + + foreach ($this->getSubjectGroupsProperty() as $group) { + $flat += $group['subjects']; + } + + return $flat; + } + + public function getSubjectPlaceholderProperty(): string + { + return (string) ($this->content['form']['subjects'][''] ?? $this->content['form']['subject_placeholder'] ?? __('ui.contact_form.subject_placeholder')); } /** @return array> */ @@ -64,7 +159,7 @@ class ContactForm extends Component 'firstName' => ['required', 'string', 'max:255'], 'lastName' => ['required', 'string', 'max:255'], 'email' => ['required', 'email:rfc', 'max:255'], - 'subject' => ['required', 'string'], + 'subject' => ['required', 'string', Rule::in(array_keys($this->subjects))], 'message' => ['required', 'string', 'max:2000'], 'company' => ['nullable', 'string', 'max:255'], 'phone' => ['nullable', 'string', 'max:255'], @@ -90,8 +185,9 @@ class ContactForm extends Component $payload['is_spam'] = $spamDetector->detect($payload, $this->formLoadedAt); $subjectLabel = $this->getSubjectsProperty()[$this->subject] ?? $this->subject; + $prefix = $this->world === 'immobilien' ? 'Kontaktanfrage B2in Immobilien' : 'Kontaktanfrage B2in'; $subjectLine = $subjectLabel - ? 'Kontaktanfrage B2in: ' . $subjectLabel + ? $prefix.': '.$subjectLabel : __('contact-form::contact-form.default_subject'); $service->handle($payload, $subjectLine, 'contact-form'); diff --git a/app/Livewire/Web/Components/UI/Footer.php b/app/Livewire/Web/Components/UI/Footer.php index e569a01e..d4cadc9d 100644 --- a/app/Livewire/Web/Components/UI/Footer.php +++ b/app/Livewire/Web/Components/UI/Footer.php @@ -6,6 +6,62 @@ use Livewire\Component; class Footer extends Component { + /** @var array */ + public array $content = []; + + /** Farbwelt der Seite (`interior` | `immobilien`), in mount() festgehalten. */ + public string $world = 'interior'; + + /** @var array */ + public array $worlds = []; + + public string $logoPath = ''; + + /** + * Negatives Logo für die Farbvariante „Modern“ (Nachtrag 18): dort liegt der Interior-Footer + * dunkel in Tanne. Die Palette schaltet im Browser, deshalb liegen beide Logos im Markup und + * das Stylesheet blendet das passende ein. Leer in der Immobilien-Welt, die ohnehin das + * negative Logo trägt. + */ + public string $logoPathDark = ''; + + /** + * Fuer die Immobilien-Welt zeigen die gemeinsamen Footer-Links auf die Seiten dieser Welt. + * + * @var array + */ + private const IMMOBILIEN_LINKS = [ + '/faq' => '/immobilien/faq', + '/about' => '/immobilien/ueber-b2in', + '/magazin' => '/immobilien/magazin', + ]; + + public function mount(): void + { + $section = cms_theme_section('footer'); + $this->content = is_array($section) ? $section : []; + $this->world = b2in_world(); + + if ($this->world === 'immobilien' && is_array($this->content['links'] ?? null)) { + $this->content['links'] = array_map(function ($link) { + if (is_array($link) && isset(self::IMMOBILIEN_LINKS[$link['url'] ?? ''])) { + $link['url'] = self::IMMOBILIEN_LINKS[$link['url']]; + } + + return $link; + }, $this->content['links']); + } + $this->worlds = b2in_worlds($this->world); + + // Der Interior-Footer liegt auf Sand (helle Fläche) und trägt deshalb das positive + // Logo; der Immobilien-Footer bleibt dunkel und nutzt die negative Variante. + $variant = $this->world === 'immobilien' ? 'negative' : 'positive'; + $this->logoPath = \App\Helpers\ThemeHelper::getLogoPath($variant, $this->world); + $this->logoPathDark = $this->world === 'immobilien' + ? '' + : \App\Helpers\ThemeHelper::getLogoPath('negative', $this->world); + } + public function render() { return view('livewire.web.components.ui.footer'); diff --git a/app/Livewire/Web/Components/UI/Header.php b/app/Livewire/Web/Components/UI/Header.php index e59a0ffe..9dc8d71d 100644 --- a/app/Livewire/Web/Components/UI/Header.php +++ b/app/Livewire/Web/Components/UI/Header.php @@ -16,13 +16,33 @@ class Header extends Component 'en' => 'EN', ]; + /** Farbwelt der Seite (`interior` | `immobilien`), in mount() festgehalten. */ + public string $world = 'interior'; + + /** @var array */ + public array $worlds = []; + + public string $logoPath = ''; + + /** Pfad der Seite, in mount() festgehalten – ein Livewire-Request traegt ihn nicht mehr. */ + public string $currentPath = '/'; + + /** Ziel des Kontakt-Buttons: /contact (Interior) bzw. /immobilien/kontakt (Immobilien). */ + public string $contactUrl = ''; + public function mount(): void { $this->domainName = \App\Helpers\ThemeHelper::getDomainName(); $this->domainUrl = \App\Helpers\ThemeHelper::getDomainUrl(); $this->currentLocale = app()->getLocale(); + $this->world = b2in_world(); + $this->worlds = b2in_worlds($this->world); + $this->logoPath = \App\Helpers\ThemeHelper::getLogoPath('positive', $this->world); + $this->currentPath = '/'.ltrim(request()->path(), '/'); + $this->contactUrl = route($this->world === 'immobilien' ? 'immobilien.contact' : 'contact'); - $this->content = cms_theme_section('header'); + // Jede Welt hat ihre eigene Navigation (CMS: global.header bzw. global.header_immobilien). + $this->content = cms_theme_section($this->world === 'immobilien' ? 'header_immobilien' : 'header'); // Ensure required keys exist if (!isset($this->content['portal_login'])) { @@ -57,27 +77,43 @@ class Header extends Component { $this->isMobileMenuOpen = false; } - public function isActiveRoute($url) + /** + * Aktiver Navigationspunkt: exakter Treffer, sonst der laengste Eintrag, dessen Pfad ein + * Praefix der Seite ist. So markiert /immobilien/magazin nur „Magazin“, nicht auch + * „Immobilien“. Anker (#…) zaehlen nicht zum Pfad. + */ + public function isActiveRoute($url): bool { - $currentPath = request()->path(); - $currentPath = '/' . ltrim($currentPath, '/'); + $path = $this->navPath((string) $url); - // Exact match - if ($currentPath === $url) { + if ($this->currentPath === $path) { return true; } - // Special handling for home route - if ($url === '/' && ($currentPath === '/' || $currentPath === '')) { - return true; + if ($path === '/' || ! str_starts_with($this->currentPath, rtrim($path, '/').'/')) { + return false; } - // Check if current path starts with the navigation URL (for sub-pages) - if ($url !== '/' && str_starts_with($currentPath, rtrim($url, '/'))) { - return true; + foreach ($this->content['navigation'] ?? [] as $navItem) { + $other = $this->navPath((string) ($navItem['url'] ?? '')); + + if ($other === $this->currentPath) { + return false; + } + + if (strlen($other) > strlen($path) && str_starts_with($this->currentPath, rtrim($other, '/').'/')) { + return false; + } } - return false; + return true; + } + + private function navPath(string $url): string + { + $path = (string) (parse_url($url, PHP_URL_PATH) ?? '/'); + + return '/'.ltrim($path, '/'); } public function render() { diff --git a/app/Models/CmsArticle.php b/app/Models/CmsArticle.php index 28c3fa11..f5e1d92c 100644 --- a/app/Models/CmsArticle.php +++ b/app/Models/CmsArticle.php @@ -13,12 +13,47 @@ class CmsArticle extends Model protected $table = 'cms_articles'; + /** + * Alte numerische Artikel-IDs (resources/lang/{locale}/b2in.php) → Slug. + * + * Wird an drei Stellen gebraucht: beim Anlegen durch den CmsArticleSeeder, für den + * Lang-Fallback und für die 301-Weiterleitung der alten /magazin/{id}-URLs. + * + * @var array + */ + public const LEGACY_ID_TO_SLUG = [ + 1 => 'escrow-system-dubai-investoren', + 2 => 'spotlight-al-jaddaf-hotspot', + 3 => 'turnkey-investments-moeblierung-rendite', + 4 => 'supply-chain-management-vertragssicherheit', + 5 => 'local-for-local-regionaler-moebelhandel', + ]; + + public const WORLD_INTERIOR = 'interior'; + + public const WORLD_IMMOBILIEN = 'immobilien'; + + /** @var array */ + public const WORLDS = [self::WORLD_INTERIOR, self::WORLD_IMMOBILIEN]; + + /** + * Beiträge aus dem Bestand, die zur Immobilien-Welt gehören (Dubai-Themen). + * + * @var array + */ + public const LEGACY_IMMOBILIEN_SLUGS = [ + 'escrow-system-dubai-investoren', + 'spotlight-al-jaddaf-hotspot', + 'turnkey-investments-moeblierung-rendite', + ]; + protected $fillable = [ 'slug', 'title', 'subtitle', 'image', 'category', + 'world', 'date_label', 'read_time', 'author', @@ -53,6 +88,104 @@ class CmsArticle extends Model return $query->orderBy('order')->orderByDesc('created_at'); } + public function scopeForWorld(Builder $query, string $world): Builder + { + return $query->where('world', $world); + } + + /** + * Veröffentlichte Artikel fürs Frontend, nach Slug indiziert. + * + * Das CMS gewinnt. Solange keine Artikel gepflegt sind, greift die Lang-Datei als Fallback – + * dieselbe Reihenfolge wie bei cms_theme_section(), damit das Magazin auf einer frischen + * Installation nicht leer ist. + * + * Mit `$world` nur die Beiträge dieser Farbwelt (Interior oder Immobilien) – die beiden + * Magazinlisten sind getrennt, die Detailseite rendert in der Welt ihres Beitrags. + * + * @return array> + */ + public static function forFrontend(?string $world = null): array + { + $maintained = static::query()->published()->exists(); + + if ($maintained) { + return static::query() + ->published() + ->when($world !== null, fn (Builder $query) => $query->forWorld($world)) + ->ordered() + ->get() + ->mapWithKeys(fn (self $article) => [$article->slug => $article->toFrontendArray()]) + ->all(); + } + + $articles = static::fallbackFromLang(); + + if ($world === null) { + return $articles; + } + + return array_filter($articles, fn (array $article) => ($article['world'] ?? self::WORLD_INTERIOR) === $world); + } + + /** + * Slug zu einer alten numerischen Magazin-ID, falls bekannt. + */ + public static function slugForLegacyId(int|string $id): ?string + { + return self::LEGACY_ID_TO_SLUG[(int) $id] ?? null; + } + + /** + * Slug eines Artikels aus der Lang-Datei. + * + * Neue Artikel bringen ihren Slug selbst mit (`'slug' => …`). Die fünf ursprünglichen + * Artikel hatten keinen und werden weiter über {@see LEGACY_ID_TO_SLUG} aufgelöst – die + * Konstante bleibt damit das, was sie ist: die Karte für die alten `/magazin/{id}`-URLs. + * + * @param array $article + */ + public static function slugForLangArticle(int|string $id, array $article): ?string + { + $slug = $article['slug'] ?? null; + + if (is_string($slug) && $slug !== '') { + return $slug; + } + + return self::slugForLegacyId($id); + } + + /** + * @return array> + */ + private static function fallbackFromLang(): array + { + $articles = trans('b2in.articles'); + + if (! is_array($articles)) { + return []; + } + + $out = []; + + foreach ($articles as $id => $article) { + if (! is_array($article)) { + continue; + } + + $slug = self::slugForLangArticle($id, $article); + + if ($slug === null) { + continue; + } + + $out[$slug] = $article + ['slug' => $slug, 'world' => self::WORLD_INTERIOR]; + } + + return $out; + } + /** * @return array */ @@ -65,6 +198,7 @@ class CmsArticle extends Model 'subtitle' => $this->subtitle, 'image' => $this->image, 'category' => $this->category, + 'world' => $this->world ?? self::WORLD_INTERIOR, 'date' => $this->date_label, 'readTime' => $this->read_time, 'author' => $this->author ?? [], diff --git a/app/Models/CmsBrand.php b/app/Models/CmsBrand.php new file mode 100644 index 00000000..5a724ba3 --- /dev/null +++ b/app/Models/CmsBrand.php @@ -0,0 +1,348 @@ + */ + 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 */ + 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> + */ + 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> + */ + 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, claim: string, quote: string, quote_author: string, detail: array{title: string, paragraphs: list}|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 + */ + 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 + */ + 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, company: list, inspirations: list} + */ + 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 + */ + 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, + ]; + } +} diff --git a/app/Models/CmsReference.php b/app/Models/CmsReference.php new file mode 100644 index 00000000..f5b7492d --- /dev/null +++ b/app/Models/CmsReference.php @@ -0,0 +1,115 @@ + */ + public array $translatable = ['title', 'description']; + + protected function casts(): array + { + return [ + 'gallery' => 'array', + 'brand_slugs' => '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'); + } + + /** + * Galerie in eine feste Form bringen: nur Einträge mit Bild, Bildunterschrift getrimmt. + * + * @param mixed $gallery + * @return list + */ + public static function normalizeGallery($gallery): array + { + if (! is_array($gallery)) { + return []; + } + + $out = []; + + foreach ($gallery as $item) { + $image = trim((string) (is_array($item) ? ($item['image'] ?? '') : $item)); + + if ($image === '') { + continue; + } + + $out[] = [ + 'image' => $image, + 'caption' => is_array($item) ? trim((string) ($item['caption'] ?? '')) : '', + ]; + } + + return $out; + } + + /** + * Beteiligte Marken als Liste mit Name und Link, aufgelöst gegen die veröffentlichten Marken. + * + * Eine unveröffentlichte oder gelöschte Marke fällt heraus. + * + * @param array> $brands Ergebnis von CmsBrand::forFrontend() + * @return list + */ + public function resolveBrands(array $brands): array + { + $out = []; + + foreach ($this->brand_slugs ?? [] as $slug) { + $slug = (string) $slug; + + if (! isset($brands[$slug])) { + continue; + } + + $out[] = [ + 'slug' => $slug, + 'name' => (string) ($brands[$slug]['name'] ?? $slug), + 'link' => ! empty($brands[$slug]['has_detail_page']) ? '/marken/'.$slug : null, + ]; + } + + return $out; + } +} diff --git a/app/Providers/ThemeServiceProvider.php b/app/Providers/ThemeServiceProvider.php index 5341c30e..a3ffcac6 100644 --- a/app/Providers/ThemeServiceProvider.php +++ b/app/Providers/ThemeServiceProvider.php @@ -57,6 +57,18 @@ class ThemeServiceProvider extends ServiceProvider $domainConfig = array_merge($domainConfig, $confiDomains[$themeOverride]); } + // Unbekannter Host (Vorschau-, Staging- oder IP-Aufruf): Bis hierher stünde in `url` noch + // APP_URL bzw. der Vorgabewert aus config/domains.php – also eine *fremde* Domain. Alle + // Assets und Links würden dorthin zeigen, obwohl die Instanz unter einem anderen Namen + // läuft. Außerhalb von `production` gilt deshalb der tatsächliche Request-Origin. + // + // In `production` bleibt es bewusst bei der konfigurierten URL: dort darf ein gefälschter + // Host-Header keine Links auf eine fremde Domain erzeugen. + if ($domainConfig['domain_name'] !== $host && config('app.env') !== 'production') { + $domainConfig['domain_name'] = $host; + $domainConfig['url'] = Request::getSchemeAndHttpHost(); + } + // Dynamische ASSET_URL basierend auf der aktuellen Domain setzen // Verhindert CORS-Probleme, da Assets immer von derselben Domain geladen werden $assetUrl = $domainConfig['url']; diff --git a/app/Services/CmsPageTree.php b/app/Services/CmsPageTree.php new file mode 100644 index 00000000..92ffc905 --- /dev/null +++ b/app/Services/CmsPageTree.php @@ -0,0 +1,187 @@ +>}> + */ + public function pages(): array + { + $rows = CmsContent::query() + ->get(['id', 'group', 'key']) + ->keyBy(fn (CmsContent $row) => $row->group.'/'.$row->key); + + $pages = []; + + foreach (config('cms_pages.pages', []) as $pageKey => $page) { + $sections = []; + + foreach ($page['sections'] ?? [] as $section) { + $id = $section['group'].'/'.$section['key']; + + $sections[] = array_merge($section, [ + 'page' => $pageKey, + 'path' => $section['path'] ?? $page['path'] ?? null, + 'anchor' => $section['anchor'] ?? null, + 'content_id' => $rows[$id]->id ?? null, + 'theme_key' => $this->themeKey($section['group'], $section['key']), + ]); + } + + $pages[$pageKey] = [ + 'title' => $page['title'], + 'path' => $page['path'] ?? null, + 'sections' => $sections, + ]; + } + + $unlisted = $this->unlistedSections($rows->keys()->all(), $rows); + + if ($unlisted !== []) { + $pages[self::UNLISTED] = [ + 'title' => config('cms_pages.unlisted.title', 'Nicht mehr verwendet'), + 'path' => null, + 'sections' => $unlisted, + ]; + } + + return $pages; + } + + /** + * Eine Sektion aus der Registry samt Datenbankbezug, oder null. + * + * @return array|null + */ + public function section(string $group, string $key): ?array + { + foreach ($this->pages() as $page) { + foreach ($page['sections'] as $section) { + if ($section['group'] === $group && $section['key'] === $key) { + return $section; + } + } + } + + return null; + } + + /** + * Klartext-Beschriftung eines Feldes: Sektions-Override, dann Wörterbuch, sonst lesbar gemacht. + */ + public function fieldLabel(string $field, ?string $group = null, ?string $key = null): string + { + if ($group !== null && $key !== null) { + foreach (config('cms_pages.pages', []) as $page) { + foreach ($page['sections'] ?? [] as $section) { + if ($section['group'] === $group && $section['key'] === $key && isset($section['fields'][$field])) { + return $section['fields'][$field]; + } + } + } + } + + $labels = config('cms_pages.field_labels', []); + + if (isset($labels[$field])) { + return $labels[$field]; + } + + return Str::ucfirst(str_replace('_', ' ', $field)); + } + + /** + * Adresse der Vorschau für eine Sektion: Frontend-Domain, Pfad, Cache-Brecher und Sprungmarke. + */ + public function previewUrl(?array $section, int $version = 0): ?string + { + $path = $section['path'] ?? null; + + if ($path === null) { + return null; + } + + $url = rtrim(config('domains.protocol', 'https://').config('domains.domain_b2in'), '/') + .'/'.ltrim($path, '/') + .'?cms-preview='.$version; + + if (! empty($section['anchor'])) { + $url .= '#'.$section['anchor']; + } + + return $url; + } + + /** + * Theme-Section-Schlüssel (Lang-Datei) zu Gruppe/Key, für „Aus Vorlage anlegen“. + */ + public function themeKey(string $group, string $key): ?string + { + foreach (config('cms_section_map.sections', []) as $themeKey => $target) { + if ($target === [$group, $key]) { + return $themeKey; + } + } + + return null; + } + + /** + * @param list $ids + * @param \Illuminate\Support\Collection $rows + * @return list> + */ + private function unlistedSections(array $ids, $rows): array + { + $listed = []; + + foreach (config('cms_pages.pages', []) as $page) { + foreach ($page['sections'] ?? [] as $section) { + $listed[$section['group'].'/'.$section['key']] = true; + } + } + + $out = []; + + foreach ($ids as $id) { + if (isset($listed[$id])) { + continue; + } + + $row = $rows[$id]; + + $out[] = [ + 'group' => $row->group, + 'key' => $row->key, + 'page' => self::UNLISTED, + 'path' => null, + 'anchor' => null, + 'title' => $row->group.' / '.$row->key, + 'description' => null, + 'content_id' => $row->id, + 'theme_key' => null, + ]; + } + + usort($out, fn (array $a, array $b) => [$a['group'], $a['key']] <=> [$b['group'], $b['key']]); + + return $out; + } +} diff --git a/app/Support/DisplayModuleSettings.php b/app/Support/DisplayModuleSettings.php index de9badb5..3bf52bc8 100644 --- a/app/Support/DisplayModuleSettings.php +++ b/app/Support/DisplayModuleSettings.php @@ -27,8 +27,10 @@ class DisplayModuleSettings ], DisplayVersionType::B2in->value => [ 'theme' => 'dark', - 'header_logo_url' => '/_cabinet/assets/b2in-logo-positive.svg', - 'header_claim' => 'Connecting Design & Property', + // Standard-Theme ist dunkel, das Logo liegt dort auf einem dunklen Scrim – + // deshalb die negative (weisse) Variante. + 'header_logo_url' => '/_cabinet/assets/b2in-interior-logo-negative.svg', + 'header_claim' => 'Premium-Einrichtung. Persönlich gemacht.', 'logo_position' => 'top-left', 'claim_position' => 'top-right', 'show_logo' => true, diff --git a/app/helpers.php b/app/helpers.php index 31949547..5bf90149 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -66,6 +66,35 @@ if (! function_exists('cms_theme_section')) { } } +if (! function_exists('whatsapp_link')) { + /** + * Klick-zum-Chat-Link, sofern im CMS eine Nummer hinterlegt ist (global.contact.whatsapp). + * + * Liefert einen leeren String, solange keine Nummer bestaetigt ist – der Button erscheint dann + * gar nicht erst, statt auf eine erfundene Nummer zu zeigen. + */ + function whatsapp_link(): string + { + $service = app(CmsContentService::class); + + // Die Nummer kann als eigener Eintrag oder als Feld der Sektion `global.contact` liegen. + $number = $service->getIfExists('global.contact.whatsapp'); + + if ($number === null) { + $contact = $service->getIfExists('global.contact'); + $number = is_array($contact) ? ($contact['whatsapp'] ?? null) : null; + } + + if (! is_string($number)) { + return ''; + } + + $digits = preg_replace('/\D+/', '', $number); + + return $digits === '' ? '' : 'https://wa.me/'.$digits; + } +} + if (! function_exists('theme_image_url')) { /** * Bild-URL für Theme-/CMS-Inhalte: zuerst Medienbibliothek (CmsMedia, oft nur Dateiname z. B. *.webp), @@ -177,3 +206,80 @@ if (! function_exists('media_url')) { return $resolved[$cacheKey]; } } + +if (! function_exists('b2in_world')) { + /** + * Farbwelt der aktuellen Seite: `interior` (Standard) oder `immobilien`. + * + * Die Immobilien-Welt umfasst die Immobilien-Routen samt Detailseiten und den + * Entwicklungs-Vorschauen. Das Layout setzt den Wert als Body-Klasse + * (`world-interior` / `world-immobilien`), theme-b2in.css schaltet daran die Farben um. + * Livewire-Komponenten merken sich die Welt in mount(), weil ein späterer + * Livewire-Request keine Seitenroute mehr trägt. + */ + function b2in_world(): string + { + if (app()->bound('b2in.world')) { + return (string) app('b2in.world'); + } + + $routeName = request()->route()?->getName(); + + if (is_string($routeName) && (str_starts_with($routeName, 'immobilien') || str_starts_with($routeName, 'dev.immobilien'))) { + return 'immobilien'; + } + + return 'interior'; + } +} + +if (! function_exists('b2in_palette')) { + /** + * Abweichende Farbpalette der aktuellen Seite – leer im Normalfall. + * + * Der Entwurf der Startseite (Nachtrag 19, /entwurf) bindet „greige“; Logo und Layout + * richten sich danach. Die Wahl „Bisher/Neu“ des Farb-Schalters läuft dagegen im Browser + * und taucht hier nicht auf. + */ + function b2in_palette(): string + { + return app()->bound('b2in.palette') ? (string) app('b2in.palette') : ''; + } +} + +if (! function_exists('b2in_worlds')) { + /** + * Die beiden Welten für den Welten-Wechsler in Kopf- und Fußzeile. + * + * @return array + */ + function b2in_worlds(?string $current = null): array + { + $current = $current ?? b2in_world(); + + return [ + 'interior' => [ + 'label' => __('ui.world_interior'), + 'url' => url('/'), + 'active' => $current === 'interior', + ], + 'immobilien' => [ + 'label' => __('ui.world_immobilien'), + 'url' => route('immobilien'), + 'active' => $current === 'immobilien', + ], + ]; + } +} + +if (! function_exists('b2in_world_set')) { + /** + * Welt für den laufenden Request festlegen, wenn sie nicht an der Route hängt – + * etwa auf der Magazin-Detailseite, die in der Welt ihres Beitrags rendert. + * Muss vor dem Rendern von Kopf- und Fußzeile geschehen (beide halten die Welt in mount() fest). + */ + function b2in_world_set(string $world): void + { + app()->instance('b2in.world', $world); + } +} diff --git a/config/app.php b/config/app.php index 8e208bf3..0e27f5c9 100644 --- a/config/app.php +++ b/config/app.php @@ -41,6 +41,60 @@ return [ 'debug' => (bool) env('APP_DEBUG', false), + /* + |-------------------------------------------------------------------------- + | Indexierung durch Suchmaschinen + |-------------------------------------------------------------------------- + | + | Nur die Produktivumgebung darf indexiert werden. Vorschau- und Testinstanzen + | liefern damit automatisch `noindex, nofollow` – sonst konkurriert eine Kopie + | der Website mit dem Original um dieselben Suchergebnisse. + | + | Bewusst als eigener Schalter und nicht nur an APP_ENV gekoppelt, damit eine + | Vorschau notfalls auch mit APP_ENV=production noindex bleiben kann. + | + */ + 'indexable' => (bool) env('SITE_INDEXABLE', env('APP_ENV', 'production') === 'production'), + + /* + |-------------------------------------------------------------------------- + | Farbvarianten-Schalter (B2in Interior) + |-------------------------------------------------------------------------- + | + | Blendet auf den Interior-Seiten den schwimmenden Schalter ein, mit dem sich + | die bisherige Creme-Palette und die hellere Variante „Modern“ vergleichen + | lassen (Nachtrag 18). Nur für die Abstimmung gedacht – sobald eine Palette + | entschieden ist, per B2IN_PALETTE_SWITCH=false abschalten. + | + */ + + 'palette_switch' => (bool) env('B2IN_PALETTE_SWITCH', false), + + /* + |-------------------------------------------------------------------------- + | Feste Farbpalette der Interior-Welt (ohne Schalter) + |-------------------------------------------------------------------------- + | + | „modern“ = die hellere Variante „Neu“ (Nachtrag 18), „classic“ = Creme. + | Gilt, solange der Schalter aus ist; mit Schalter ist es die Vorbelegung. + | Seit der Abstimmung 25.09. steht die Seite auf „modern“. + | + */ + + 'palette' => env('B2IN_PALETTE', 'modern'), + + /* + |-------------------------------------------------------------------------- + | Entwurf der Startseite unter /entwurf + |-------------------------------------------------------------------------- + | + | Farbwelt „Greige“ mit Hero-Slideshow (Nachtrag 19), nur zur Abstimmung. + | Abschalten, sobald der Entwurf entschieden ist. + | + */ + + 'home_draft' => (bool) env('B2IN_HOME_DRAFT', true), + /* |-------------------------------------------------------------------------- | Application URL diff --git a/config/cms_pages.php b/config/cms_pages.php new file mode 100644 index 00000000..ab7d0583 --- /dev/null +++ b/config/cms_pages.php @@ -0,0 +1,299 @@ + [ + 'title' => 'Überschrift', + 'subtitle' => 'Untertitel', + 'headline' => 'Überschrift', + 'tagline' => 'Claim', + 'text' => 'Text', + 'intro' => 'Einleitung', + 'description' => 'Beschreibung', + 'content' => 'Inhalt', + 'lead' => 'Vorspann', + 'note' => 'Hinweis', + 'label' => 'Beschriftung', + 'badge' => 'Kennzeichen', + 'eyebrow' => 'Überzeile', + 'quote' => 'Zitat', + 'author' => 'Zitatgeber', + 'name' => 'Name', + 'role' => 'Rolle / Funktion', + 'city' => 'Ort', + 'value' => 'Wert', + 'number' => 'Zahl', + 'suffix' => 'Zusatz', + 'icon' => 'Icon', + 'image' => 'Bild', + 'image_alt' => 'Bildbeschreibung', + 'alt' => 'Bildbeschreibung', + 'logo' => 'Logo', + 'link' => 'Link', + 'url' => 'Adresse (URL)', + 'href' => 'Link', + 'link_text' => 'Linktext', + 'button_text' => 'Button-Beschriftung', + 'button_link' => 'Button-Link', + 'primary_button' => 'Erster Button', + 'secondary_button' => 'Zweiter Button', + 'cta_title' => 'Überschrift Abschluss', + 'cta_text' => 'Text Abschluss', + 'cta_link' => 'Link Abschluss', + 'items' => 'Einträge', + 'steps' => 'Schritte', + 'cards' => 'Karten', + 'stats' => 'Kennzahlen', + 'features' => 'Merkmale', + 'benefits' => 'Vorteile', + 'values' => 'Werte', + 'links' => 'Links', + 'columns' => 'Spalten', + 'menu' => 'Menüpunkte', + 'navigation' => 'Navigation', + 'topics' => 'Themen', + 'options' => 'Auswahlmöglichkeiten', + 'placeholder' => 'Platzhaltertext', + 'meta_title' => 'Seitentitel (Browser, Google)', + 'meta_description' => 'Beschreibung für Google', + 'email' => 'E-Mail', + 'phone' => 'Telefon', + 'whatsapp' => 'WhatsApp-Nummer', + 'address' => 'Adresse', + 'opening_hours' => 'Öffnungszeiten', + 'copyright' => 'Copyright-Zeile', + 'map_alt' => 'Beschreibung der Karte', + 'region_label' => 'Beschriftung der Region', + 'distance_suffix' => 'Zusatz hinter der Entfernung', + 'hub' => 'Drehscheibe (Bielefeld)', + 'steps_title' => 'Überschrift Ablauf-Block', + 'steps_text' => 'Einleitung Ablauf-Block', + 'x' => 'Position waagerecht (Entwickler)', + 'y' => 'Position senkrecht (Entwickler)', + 'id' => 'Sprungmarke (Entwickler)', + 'slug' => 'Adresskürzel (Entwickler)', + 'teaser' => 'Kurztext', + 'summary' => 'Zusammenfassung', + 'brands' => 'Marken', + 'projects' => 'Projekte', + 'partners' => 'Partner', + 'questions' => 'Fragen', + 'question' => 'Frage', + 'answer' => 'Antwort', + 'read_more' => 'Weiterlesen-Text', + 'empty_text' => 'Text, wenn nichts vorhanden ist', + 'success_text' => 'Text nach dem Absenden', + 'submit_text' => 'Beschriftung Absenden-Button', + 'privacy_text' => 'Datenschutz-Hinweis', + 'subjects' => 'Betreffe', + 'subject' => 'Betreff', + ], + + /* + |-------------------------------------------------------------------------- + | Seiten in der Reihenfolge des Menüs + |-------------------------------------------------------------------------- + */ + 'pages' => [ + + 'home' => [ + 'title' => 'Startseite', + 'path' => '/', + 'sections' => [ + ['group' => 'home', 'key' => 'hero', 'anchor' => 'hero', 'title' => 'Hero', + 'description' => 'Der große Einstieg ganz oben: Überschrift, Text, Buttons und Bild.'], + ['group' => 'home', 'key' => 'hero_slider', 'anchor' => 'hero', 'title' => 'Hero mit Slideshow (Entwurf)', + 'path' => '/entwurf', + 'description' => 'Der Hero des Entwurfs vom 24.09.: Überschrift, Text, Button und die Slideshow. Liste „slides“ leer = Titelbilder der Kernmarken; sonst je Eintrag image, label, caption und active.'], + ['group' => 'home', 'key' => 'founder_bar', 'anchor' => 'gruender', 'title' => 'Gründer-Leiste', + 'description' => 'Schmale Zeile mit Marcel Scheibe und Zitat. Steht auch auf „Über uns“ und in der Immobilien-Welt.'], + ['group' => 'home', 'key' => 'audience_switch', 'anchor' => 'wege', 'title' => 'Zielgruppen-Wahl', + 'description' => 'Zwei Wege: Privat einrichten oder Projekt ausstatten.'], + ['group' => 'home', 'key' => 'core_brands', 'anchor' => 'marken', 'title' => 'Kernmarken', + 'description' => 'Überschrift und Untertitel des Marken-Rasters. Die Kacheln selbst kommen aus der Markenverwaltung.'], + ['group' => 'home', 'key' => 'origin_section', 'anchor' => 'herkunft', 'title' => 'Aus der Region gefertigt', + 'description' => 'Karte mit Bielefeld als Drehscheibe und der Ablauf-Block am Beispiel CABINET. Die Standorte pflegst du bei den Marken. Steht auch auf /marken.'], + ['group' => 'home', 'key' => 'vision_section', 'anchor' => 'vision', 'title' => 'Gebaut auf Expertise', + 'description' => 'Die Geschichte hinter B2in: Text, Kennzahlen und Bild.'], + ['group' => 'home', 'key' => 'sponsoring', 'anchor' => 'engagement', 'title' => 'Regional verbunden', + 'description' => 'Vereine und Organisationen, die B2in unterstützt.'], + ['group' => 'referenzen', 'key' => 'display_feed', 'anchor' => 'displays', 'title' => 'Aus den Stores', + 'description' => 'Überschrift und Text des Display-Feeds. Die Inhalte kommen aus den Store Displays. Steht auch auf /referenzen.'], + ['group' => 'home', 'key' => 'immobilien_teaser', 'anchor' => 'immobilien', 'title' => 'Immobilien-Teaser', + 'description' => 'Der Übergang in die Immobilien-Welt am Ende der Startseite.'], + ['group' => 'global', 'key' => 'cta_section', 'anchor' => 'cta', 'title' => 'Abschluss: Termin anfragen', + 'description' => 'Der letzte Block vor dem Fuß. Steht auf allen Interior-Seiten.'], + ], + ], + + 'marken' => [ + 'title' => 'Marken', + 'path' => '/marken', + 'sections' => [ + ['group' => 'marken', 'key' => 'hero', 'anchor' => 'seitenkopf', 'title' => 'Seitenkopf', + 'description' => 'Überschrift, Einleitung und Bild oben auf /marken. Die Markenblöcke darunter kommen aus der Markenverwaltung.'], + ['group' => 'marken', 'key' => 'network', 'anchor' => 'netzwerk', 'title' => 'Mehr als Marken: unser Netzwerk', + 'description' => 'Partner und Zulieferer jenseits der Kernmarken.'], + ['group' => 'marken', 'key' => 'growth', 'anchor' => 'wachstum', 'title' => 'Die Markenwelt wächst', + 'description' => 'Hinweis, dass weitere Marken folgen.'], + ['group' => 'marken', 'key' => 'cta', 'anchor' => 'marken-cta', 'title' => 'Abschluss auf /marken', + 'description' => 'Der Abschlussblock „Lernen Sie unsere Marken kennen“ mit Terminanfrage und WhatsApp.'], + ['group' => 'marken', 'key' => 'craft', 'anchor' => 'handwerk', 'title' => 'Handwerk-Abschluss auf jeder Markenseite', + 'description' => 'Der letzte Abschnitt auf den Seiten von CABINET, Rotpunkt und Febrü: „Planung plus Handwerk“ mit Link zu Otterpohl. Einmal gepflegt, überall gleich.', 'path' => '/marken/cabinet'], + ], + ], + + 'referenzen' => [ + 'title' => 'Referenzen', + 'path' => '/referenzen', + 'sections' => [ + ['group' => 'referenzen', 'key' => 'hero', 'anchor' => 'seitenkopf', 'title' => 'Seitenkopf', + 'description' => 'Überschrift und Einleitung der Referenzen-Seite.'], + ['group' => 'referenzen', 'key' => 'projects', 'anchor' => 'projekte', 'title' => 'Realisierte Projekte', + 'description' => 'Überschrift, Text und Leerzustand über den Projekten. Die Projekte selbst pflegst du unter „Referenzen“.'], + ], + ], + + 'magazin' => [ + 'title' => 'Magazin', + 'path' => '/magazin', + 'sections' => [ + ['group' => 'magazin', 'key' => 'list', 'anchor' => 'magazin', 'title' => 'Magazin-Übersicht', + 'description' => 'Überschrift und Einleitung der Übersicht. Die Beiträge pflegst du unter „Magazin“.'], + ['group' => 'magazin', 'key' => 'detail', 'anchor' => null, 'title' => 'Beitragsseite', + 'description' => 'Feste Texte auf jeder Beitragsseite, etwa „Zurück zur Übersicht“ und „Weitere Beiträge“.'], + ], + ], + + 'about' => [ + 'title' => 'Über uns', + 'path' => '/about', + 'sections' => [ + ['group' => 'about', 'key' => 'hero', 'anchor' => 'seitenkopf', 'title' => 'Seitenkopf', + 'description' => 'Überschrift und Einleitung von „Über uns“.'], + ['group' => 'about', 'key' => 'our_story', 'anchor' => 'geschichte', 'title' => 'Unsere Geschichte', + 'description' => 'Der Text zur Entstehung von B2in.'], + ['group' => 'about', 'key' => 'image_break', 'anchor' => 'bild', 'title' => 'Bildpause', + 'description' => 'Großes Bild mit kurzer Zeile zwischen Geschichte und Werten.'], + ['group' => 'about', 'key' => 'our_values', 'anchor' => 'werte', 'title' => 'Unsere Werte', + 'description' => 'Die Werte-Kacheln.'], + ['group' => 'netzwerk', 'key' => 'partner_cta', 'anchor' => 'cta', 'title' => 'Abschluss auf „Über uns“', + 'description' => 'Der Abschlussblock am Ende von „Über uns“.'], + ], + ], + + 'contact' => [ + 'title' => 'Kontakt', + 'path' => '/contact', + 'sections' => [ + ['group' => 'contact', 'key' => 'contact_form', 'anchor' => 'kontakt', 'title' => 'Kontaktformular', + 'description' => 'Überschrift, Betreffe, Feldbeschriftungen und Datenschutz-Hinweis des Formulars.'], + ['group' => 'global', 'key' => 'contact', 'anchor' => 'kontakt', 'title' => 'Kontaktdaten', + 'description' => 'Telefon, E-Mail, Adresse und die WhatsApp-Nummer. Ohne WhatsApp-Nummer erscheint kein WhatsApp-Button.'], + ], + ], + + 'faq' => [ + 'title' => 'FAQ', + 'path' => '/faq', + 'sections' => [ + ['group' => 'faq', 'key' => 'faq', 'anchor' => 'faq', 'title' => 'Fragen und Antworten', + 'description' => 'Überschrift und die Liste der Fragen der Interior-Welt.'], + ], + ], + + 'global' => [ + 'title' => 'Auf jeder Seite', + 'path' => '/', + 'sections' => [ + ['group' => 'global', 'key' => 'announcement_bar', 'anchor' => 'header', 'title' => 'Hinweisleiste', + 'description' => 'Schmale Zeile ganz oben über dem Menü. Leer lassen, um sie auszublenden.'], + ['group' => 'global', 'key' => 'header', 'anchor' => 'header', 'title' => 'Hauptmenü', + 'description' => 'Menüpunkte und Buttons im Kopf der Interior-Welt.'], + ['group' => 'global', 'key' => 'appointment_widget', 'anchor' => null, 'title' => 'Terminanfrage-Widget', + 'description' => 'Das schwebende Widget rechts unten: Texte, Themen und Zeitfenster.'], + ['group' => 'global', 'key' => 'footer', 'anchor' => 'footer', 'title' => 'Fußbereich', + 'description' => 'Spalten, Links, Social-Media und Copyright-Zeile.'], + ], + ], + + 'immobilien' => [ + 'title' => 'Immobilien-Welt', + 'path' => '/immobilien', + 'sections' => [ + ['group' => 'global', 'key' => 'announcement_bar_immobilien', 'anchor' => 'header', 'title' => 'Hinweisleiste (Immobilien)', + 'description' => 'Hinweisleiste über dem Menü der Immobilien-Welt.'], + ['group' => 'global', 'key' => 'header_immobilien', 'anchor' => 'header', 'title' => 'Hauptmenü (Immobilien)', + 'description' => 'Menüpunkte und Buttons im Kopf der Immobilien-Welt.'], + ['group' => 'global', 'key' => 'cta_section_immobilien', 'anchor' => 'cta', 'title' => 'Abschluss (Immobilien)', + 'description' => 'Der letzte Block vor dem Fuß auf allen Immobilien-Seiten.'], + ['group' => 'immobilien', 'key' => 'magazin_list', 'anchor' => 'magazin', 'title' => 'Magazin-Übersicht (Immobilien)', + 'description' => 'Überschrift und Einleitung unter /immobilien/magazin.', 'path' => '/immobilien/magazin'], + ['group' => 'immobilien', 'key' => 'about_hero', 'anchor' => 'seitenkopf', 'title' => 'Über B2in: Seitenkopf', + 'description' => 'Überschrift und Einleitung unter /immobilien/ueber-b2in.', 'path' => '/immobilien/ueber-b2in'], + ['group' => 'immobilien', 'key' => 'our_story', 'anchor' => 'geschichte', 'title' => 'Über B2in: Geschichte', + 'description' => 'Der Geschichte-Block der Immobilien-Welt.', 'path' => '/immobilien/ueber-b2in'], + ['group' => 'immobilien', 'key' => 'about_image_break', 'anchor' => 'bild', 'title' => 'Über B2in: Bildpause', + 'description' => 'Bild zwischen Geschichte und Werten.', 'path' => '/immobilien/ueber-b2in'], + ['group' => 'immobilien', 'key' => 'our_values', 'anchor' => 'werte', 'title' => 'Über B2in: Werte', + 'description' => 'Die Werte-Kacheln der Immobilien-Welt.', 'path' => '/immobilien/ueber-b2in'], + ['group' => 'faq', 'key' => 'faq_immobilien', 'anchor' => 'faq', 'title' => 'FAQ (Immobilien)', + 'description' => 'Fragen und Antworten unter /immobilien/faq.', 'path' => '/immobilien/faq'], + ['group' => 'contact', 'key' => 'contact_form_immobilien', 'anchor' => 'kontakt', 'title' => 'Kontaktformular (Immobilien)', + 'description' => 'Abweichende Texte und Betreffe unter /immobilien/kontakt. Leere Felder übernehmen den Interior-Wert.', 'path' => '/immobilien/kontakt'], + ], + ], + + 'legal' => [ + 'title' => 'Rechtliches', + 'path' => '/impressum', + 'sections' => [ + ['group' => 'legal', 'key' => 'impressum', 'anchor' => null, 'title' => 'Impressum', + 'description' => 'Pflichtangaben. Der Inhalt ist HTML.', 'path' => '/impressum'], + ['group' => 'legal', 'key' => 'privacy', 'anchor' => null, 'title' => 'Datenschutz', + 'description' => 'Datenschutzerklärung. Der Inhalt ist HTML.', 'path' => '/privacy'], + ['group' => 'legal', 'key' => 'terms', 'anchor' => null, 'title' => 'AGB', + 'description' => 'Allgemeine Geschäftsbedingungen. Der Inhalt ist HTML.', 'path' => '/terms'], + ['group' => 'legal', 'key' => 'cookie_policy', 'anchor' => null, 'title' => 'Cookie-Hinweise', + 'description' => 'Cookie-Richtlinie. Der Inhalt ist HTML.', 'path' => '/cookie-policy'], + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Sammelpunkt für alles, was in der Datenbank liegt, aber oben nicht steht + |-------------------------------------------------------------------------- + */ + 'unlisted' => [ + 'title' => 'Nicht mehr verwendet', + 'description' => 'Sektionen stillgelegter Seiten (Netzwerk, Partner, alte Immobilien-Seite). Bleiben als Archiv liegen, erscheinen nirgends.', + ], +]; diff --git a/config/cms_section_map.php b/config/cms_section_map.php index a2035afc..9ac818b4 100644 --- a/config/cms_section_map.php +++ b/config/cms_section_map.php @@ -14,11 +14,31 @@ return [ 'sections' => [ 'announcement_bar' => ['global', 'announcement_bar'], + 'announcement_bar_immobilien' => ['global', 'announcement_bar_immobilien'], 'header' => ['global', 'header'], + 'hero_slider' => ['home', 'hero_slider'], + 'footer' => ['global', 'footer'], 'cta_section' => ['global', 'cta_section'], + 'cta_section_immobilien' => ['global', 'cta_section_immobilien'], + 'appointment_widget' => ['global', 'appointment_widget'], 'cta_section_portfolio' => ['global', 'cta_section_portfolio'], 'hero' => ['home', 'hero'], + 'audience_switch' => ['home', 'audience_switch'], + 'core_brands' => ['home', 'core_brands'], + 'origin_section' => ['home', 'origin_section'], + + 'marken_hero' => ['marken', 'hero'], + 'marken_network' => ['marken', 'network'], + 'marken_growth' => ['marken', 'growth'], + 'marken_cta' => ['marken', 'cta'], + 'brand_craft' => ['marken', 'craft'], + + 'referenzen_hero' => ['referenzen', 'hero'], + 'reference_projects' => ['referenzen', 'projects'], + 'display_feed' => ['referenzen', 'display_feed'], + 'sponsoring' => ['home', 'sponsoring'], + 'immobilien_teaser' => ['home', 'immobilien_teaser'], 'founder_bar' => ['home', 'founder_bar'], 'synergie_section' => ['home', 'synergie_section'], 'ecosystem_core' => ['home', 'ecosystem_core'], @@ -53,8 +73,18 @@ return [ 'interior_trust' => ['netzwerk', 'interior_trust'], 'faq' => ['faq', 'faq'], + 'faq_immobilien' => ['faq', 'faq_immobilien'], + + // Immobilien-Welt: eigene Navigation, eigenes Magazin, eigene Ueber-uns-Seite + 'header_immobilien' => ['global', 'header_immobilien'], + 'magazin_list_immobilien' => ['immobilien', 'magazin_list'], + 'immobilien_about_hero' => ['immobilien', 'about_hero'], + 'immobilien_our_story' => ['immobilien', 'our_story'], + 'immobilien_about_image_break' => ['immobilien', 'about_image_break'], + 'immobilien_our_values' => ['immobilien', 'our_values'], 'contact_form' => ['contact', 'contact_form'], + 'contact_form_immobilien' => ['contact', 'contact_form_immobilien'], 'about_hero' => ['about', 'hero'], 'our_story' => ['about', 'our_story'], diff --git a/config/domains.php b/config/domains.php index c7bdac5d..a7a96401 100644 --- a/config/domains.php +++ b/config/domains.php @@ -71,13 +71,29 @@ return [ 'view_prefix' => 'b2in', 'assets_dir' => 'build/b2in', 'description' => 'Haupt-Landingpage mit Standard-Theme', + // Interior-Welt (Standard). Die Immobilien-Welt behält das bisherige Blau, + // siehe `worlds` und resources/css/web/theme-b2in.css. 'color_scheme' => [ - 'primary' => '#2b3f51', // Anthracite - 'secondary' => '#20a0da', // Dynamic Blue + 'primary' => '#202c2b', // Tanne + 'secondary' => '#d39e46', // Ocker ], 'font_family' => [ 'primary' => 'Inter', - 'secondary' => 'IBM Plex Sans', + 'secondary' => 'EB Garamond', + ], + 'worlds' => [ + 'interior' => [ + 'primary' => '#202c2b', // Tanne – Hauptton dunkel, Wortmarke + 'background' => '#f7f4ef', // Creme + 'surface' => '#d9cbbf', // Sand – Flächen, Footer + 'accent' => '#d39e46', // Ocker – Akzent, Logo-Bogen + 'secondary' => '#a38f82', // Clay – Sekundär, Produkte + 'text' => '#2d2926', // Anthrazit + ], + 'immobilien' => [ + 'primary' => '#2b3f51', // Anthracite + 'accent' => '#20a0da', // Dynamic Blue + ], ], ], diff --git a/config/seeding.php b/config/seeding.php new file mode 100644 index 00000000..9e2fe010 --- /dev/null +++ b/config/seeding.php @@ -0,0 +1,21 @@ + env('SEED_ADMIN_NAME', 'Admin'), + 'admin_email' => env('SEED_ADMIN_EMAIL', 'admin@b2in.test'), + 'admin_password' => env('SEED_ADMIN_PASSWORD', 'password'), + +]; diff --git a/database/factories/CmsArticleFactory.php b/database/factories/CmsArticleFactory.php index 53bcd566..755d4f92 100644 --- a/database/factories/CmsArticleFactory.php +++ b/database/factories/CmsArticleFactory.php @@ -23,6 +23,7 @@ class CmsArticleFactory extends Factory 'subtitle' => fake()->sentence(12), 'image' => 'b2in/magazin-'.fake()->numberBetween(1, 5).'.jpg', 'category' => fake()->randomElement(['Dubai Investment', 'Rendite & Einrichtung', 'B2B & Partner', 'Einrichtung & Netzwerk']), + 'world' => CmsArticle::WORLD_INTERIOR, 'date_label' => fake()->date('F j, Y'), 'read_time' => fake()->numberBetween(3, 10).' min read', 'author' => [ diff --git a/database/factories/CmsBrandFactory.php b/database/factories/CmsBrandFactory.php new file mode 100644 index 00000000..b27d3112 --- /dev/null +++ b/database/factories/CmsBrandFactory.php @@ -0,0 +1,70 @@ + + */ +class CmsBrandFactory extends Factory +{ + protected $model = CmsBrand::class; + + public function definition(): array + { + $name = fake()->unique()->company(); + + return [ + 'slug' => Str::slug($name).'-'.fake()->unique()->numberBetween(100, 999), + 'name' => ['de' => $name, 'en' => $name], + 'tagline' => ['de' => fake()->sentence(4), 'en' => fake()->sentence(4)], + 'teaser' => ['de' => fake()->paragraph(2), 'en' => fake()->paragraph(2)], + 'description' => ['de' => '

'.fake()->paragraph(4).'

', 'en' => '

'.fake()->paragraph(4).'

'], + 'logo' => '', + 'image' => '', + 'services' => [ + ['title' => fake()->words(3, true), 'description' => fake()->sentence(10)], + ['title' => fake()->words(3, true), 'description' => fake()->sentence(10)], + ], + 'gallery' => [], + 'website_url' => fake()->url(), + 'showroom' => 'Bielefeld', + 'is_published' => true, + 'order' => fake()->numberBetween(1, 20), + ]; + } + + /** + * Galerie mit realisierten Projekten und Inspirationen. + */ + public function withGallery(int $projects = 2, int $inspirations = 2): static + { + $items = []; + + for ($i = 1; $i <= $projects; $i++) { + $items[] = [ + 'image' => 'projekt-'.$i.'.webp', + 'category' => CmsBrand::GALLERY_PROJECT, + 'caption' => 'Projekt '.$i, + ]; + } + + for ($i = 1; $i <= $inspirations; $i++) { + $items[] = [ + 'image' => 'inspiration-'.$i.'.webp', + 'category' => CmsBrand::GALLERY_INSPIRATION, + 'caption' => 'Inspiration '.$i, + ]; + } + + return $this->state(fn () => ['gallery' => $items]); + } + + public function unpublished(): static + { + return $this->state(fn () => ['is_published' => false]); + } +} diff --git a/database/factories/CmsReferenceFactory.php b/database/factories/CmsReferenceFactory.php new file mode 100644 index 00000000..32697d74 --- /dev/null +++ b/database/factories/CmsReferenceFactory.php @@ -0,0 +1,62 @@ + + */ +class CmsReferenceFactory extends Factory +{ + protected $model = CmsReference::class; + + public function definition(): array + { + $title = fake()->unique()->words(3, true); + + return [ + 'title' => ['de' => ucfirst($title), 'en' => ucfirst($title)], + 'location' => fake()->city(), + 'year' => (string) fake()->numberBetween(2022, 2026), + 'description' => ['de' => fake()->paragraph(2), 'en' => fake()->paragraph(2)], + 'image' => 'referenzen/'.fake()->unique()->slug(2).'.webp', + 'gallery' => [], + 'brand_slugs' => [], + 'is_published' => true, + 'order' => fake()->numberBetween(1, 20), + ]; + } + + public function published(): static + { + return $this->state(fn () => ['is_published' => true]); + } + + public function unpublished(): static + { + return $this->state(fn () => ['is_published' => false]); + } + + /** + * @param list $slugs + */ + public function forBrands(array $slugs): static + { + return $this->state(fn () => ['brand_slugs' => $slugs]); + } + + public function withGallery(int $count = 2): static + { + return $this->state(function () use ($count) { + $items = []; + + for ($i = 1; $i <= $count; $i++) { + $items[] = ['image' => 'referenzen/bild-'.$i.'.webp', 'caption' => 'Bild '.$i]; + } + + return ['gallery' => $items]; + }); + } +} diff --git a/database/migrations/2026_08_28_110407_create_cms_brands_table.php b/database/migrations/2026_08_28_110407_create_cms_brands_table.php new file mode 100644 index 00000000..f15f01e1 --- /dev/null +++ b/database/migrations/2026_08_28_110407_create_cms_brands_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('slug')->unique(); + $table->json('name'); + $table->json('tagline')->nullable(); + $table->json('teaser')->nullable(); + $table->json('description')->nullable(); + $table->string('logo')->nullable(); + $table->string('image')->nullable(); + $table->json('services')->nullable(); + $table->json('gallery')->nullable(); + $table->string('website_url')->nullable(); + $table->string('showroom')->nullable(); + $table->boolean('is_published')->default(true); + $table->integer('order')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('cms_brands'); + } +}; diff --git a/database/migrations/2026_08_28_112244_add_summary_and_highlights_to_cms_brands_table.php b/database/migrations/2026_08_28_112244_add_summary_and_highlights_to_cms_brands_table.php new file mode 100644 index 00000000..9fee6347 --- /dev/null +++ b/database/migrations/2026_08_28_112244_add_summary_and_highlights_to_cms_brands_table.php @@ -0,0 +1,33 @@ +json('summary')->nullable()->after('teaser'); + $table->json('highlights')->nullable()->after('summary'); + }); + } + + public function down(): void + { + Schema::table('cms_brands', function (Blueprint $table) { + $table->dropColumn(['summary', 'highlights']); + }); + } +}; diff --git a/database/migrations/2026_09_03_084254_add_page_sections_to_cms_brands_table.php b/database/migrations/2026_09_03_084254_add_page_sections_to_cms_brands_table.php new file mode 100644 index 00000000..b786cb72 --- /dev/null +++ b/database/migrations/2026_09_03_084254_add_page_sections_to_cms_brands_table.php @@ -0,0 +1,42 @@ +json('sections')->nullable()->after('services'); + $table->json('widget_label')->nullable()->after('sections'); + $table->json('cta_title')->nullable()->after('widget_label'); + $table->json('cta_text')->nullable()->after('cta_title'); + }); + } + + public function down(): void + { + Schema::table('cms_brands', function (Blueprint $table) { + $table->dropColumn(['sections', 'widget_label', 'cta_title', 'cta_text']); + }); + } +}; diff --git a/database/migrations/2026_09_14_120000_add_world_to_cms_articles_table.php b/database/migrations/2026_09_14_120000_add_world_to_cms_articles_table.php new file mode 100644 index 00000000..ce8004e6 --- /dev/null +++ b/database/migrations/2026_09_14_120000_add_world_to_cms_articles_table.php @@ -0,0 +1,34 @@ +string('world', 20)->default(CmsArticle::WORLD_INTERIOR)->index()->after('category'); + }); + + CmsArticle::query() + ->whereIn('slug', CmsArticle::LEGACY_IMMOBILIEN_SLUGS) + ->update(['world' => CmsArticle::WORLD_IMMOBILIEN]); + } + + public function down(): void + { + Schema::table('cms_articles', function (Blueprint $table) { + $table->dropIndex(['world']); + $table->dropColumn('world'); + }); + } +}; diff --git a/database/migrations/2026_09_15_132909_add_origin_to_cms_brands_table.php b/database/migrations/2026_09_15_132909_add_origin_to_cms_brands_table.php new file mode 100644 index 00000000..59bcb806 --- /dev/null +++ b/database/migrations/2026_09_15_132909_add_origin_to_cms_brands_table.php @@ -0,0 +1,37 @@ +json('origin_city')->nullable()->after('showroom'); + $table->json('origin_distance')->nullable()->after('origin_city'); + $table->json('origin_role')->nullable()->after('origin_distance'); + }); + } + + public function down(): void + { + Schema::table('cms_brands', function (Blueprint $table) { + $table->dropColumn(['origin_city', 'origin_distance', 'origin_role']); + }); + } +}; diff --git a/database/migrations/2026_09_15_150000_create_cms_references_table.php b/database/migrations/2026_09_15_150000_create_cms_references_table.php new file mode 100644 index 00000000..7f3f6806 --- /dev/null +++ b/database/migrations/2026_09_15_150000_create_cms_references_table.php @@ -0,0 +1,44 @@ +id(); + $table->json('title'); + $table->string('location')->nullable(); + $table->string('year', 20)->nullable(); + $table->json('description')->nullable(); + $table->string('image')->nullable(); + $table->json('gallery')->nullable(); + $table->json('brand_slugs')->nullable(); + $table->boolean('is_published')->default(true); + $table->integer('order')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('cms_references'); + } +}; diff --git a/database/seeders/AudienceSwitchProjectTextSeeder.php b/database/seeders/AudienceSwitchProjectTextSeeder.php new file mode 100644 index 00000000..fad735e2 --- /dev/null +++ b/database/seeders/AudienceSwitchProjectTextSeeder.php @@ -0,0 +1,81 @@ + alter Satz → neuer Satz */ + public const REPLACEMENTS = [ + 'Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk.' => 'Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.', + 'With German contractual reliability and an established manufacturer network.' => 'With a single German contract partner and an established manufacturer network.', + ]; + + public function run(): void + { + $row = CmsContent::query()->where('group', 'home')->where('key', 'audience_switch')->first(); + + if (! $row) { + $this->command?->info('Projekttext: Sektion home/audience_switch nicht im CMS – die Lang-Datei gilt.'); + + return; + } + + $value = $row->getTranslations('value'); + $count = 0; + $value = $this->replaceDeep($value, $count); + + if ($count === 0) { + $this->command?->info('Projekttext: bereits aktuell, nichts geändert.'); + + return; + } + + $row->value = $value; + $row->save(); + app(CmsContentService::class)->clearCache('home'); + + $this->command?->info("Projekttext ersetzt ({$count} Stelle(n))."); + } + + /** + * @param array $data + * @return array + */ + private function replaceDeep(array $data, int &$count): array + { + foreach ($data as $key => $item) { + if (is_array($item)) { + $data[$key] = $this->replaceDeep($item, $count); + + continue; + } + + if (! is_string($item)) { + continue; + } + + $replaced = strtr($item, self::REPLACEMENTS); + + if ($replaced !== $item) { + $data[$key] = $replaced; + $count++; + } + } + + return $data; + } +} diff --git a/database/seeders/BrandGalleryCaptionNormalizerSeeder.php b/database/seeders/BrandGalleryCaptionNormalizerSeeder.php new file mode 100644 index 00000000..e5a435fc --- /dev/null +++ b/database/seeders/BrandGalleryCaptionNormalizerSeeder.php @@ -0,0 +1,64 @@ +whereNotNull('gallery')->get() as $brand) { + $gallery = $brand->gallery ?? []; + $changed = false; + + foreach ($gallery as $index => $item) { + $caption = (string) ($item['caption'] ?? ''); + $stripped = $this->stripPrefix($caption); + + if ($stripped !== $caption) { + $gallery[$index]['caption'] = $stripped; + $changed = true; + $cleaned++; + } + } + + if ($changed) { + $brand->gallery = CmsBrand::normalizeGallery($gallery); + $brand->save(); + } + } + + $this->command?->info("Bildunterschriften bereinigt: {$cleaned}."); + } + + private function stripPrefix(string $caption): string + { + foreach (self::PREFIXES as $prefix) { + if (str_starts_with($caption, $prefix)) { + return trim(substr($caption, strlen($prefix))); + } + } + + return $caption; + } +} diff --git a/database/seeders/BrandImageSeeder.php b/database/seeders/BrandImageSeeder.php new file mode 100644 index 00000000..c74f6acd --- /dev/null +++ b/database/seeders/BrandImageSeeder.php @@ -0,0 +1,160 @@ + …, 'category' => …] für eine andere Kategorie. + * + * @var array}> + */ + private const IMAGES = [ + 'cabinet' => [ + 'image' => 'marken/cabinet/ankleide-solitaer-eiche-tano.webp', + 'gallery' => [ + 'marken/cabinet/dachschraegenschrank-filo-cashmere.webp' => 'Dachschrägenschrank in Filo Cashmere', + 'marken/cabinet/schrank-gegen-schraege-marmor.webp' => 'Schrank gegen die Schräge in Marmoroptik', + 'marken/cabinet/schlafzimmer-eiche-tano.webp' => 'Schlafzimmerschrank in Eiche Tano', + 'marken/cabinet/schlafzimmer-velluto-fango.webp' => 'Schlafzimmerschrank in Velluto Fango', + 'marken/cabinet/schlafzimmer-wandschrank-filo-cashmere.webp' => 'Wandschrank im Schlafzimmer in Filo Cashmere', + 'marken/cabinet/garderobe-seite-offen.webp' => 'Garderobe mit offener Seite', + 'marken/cabinet/garderobe-sitznische-filo-cashmere.webp' => 'Garderobe mit Sitznische in Filo Cashmere', + 'marken/cabinet/drehtuerenschrank-marmor.webp' => 'Drehtürenschrank in Marmoroptik', + 'marken/cabinet/flur-schrank-eiche-tano.webp' => 'Flurschrank in Eiche Tano', + 'marken/cabinet/dachschraege-velluto-fango.webp' => 'Schrank unter der Dachschräge in Velluto Fango', + 'marken/cabinet/ankleide-solitaer-eiche-tano-seite.webp' => 'Ankleide Solitär in Eiche Tano, Seitenansicht', + ], + ], + 'rotpunkt' => [ + 'image' => 'marken/rotpunkt/kueche-01.webp', + 'gallery' => [ + 'marken/rotpunkt/kueche-01-b.webp' => 'Küche 01, zweite Ansicht', + 'marken/rotpunkt/kueche-02.webp' => 'Küche 02', + 'marken/rotpunkt/kueche-03.webp' => 'Küche 03', + 'marken/rotpunkt/kueche-04.webp' => 'Küche 04', + 'marken/rotpunkt/kueche-05.webp' => 'Küche 05', + 'marken/rotpunkt/kueche-06.webp' => 'Küche 06', + 'marken/rotpunkt/kueche-06-b.webp' => 'Küche 06, zweite Ansicht', + 'marken/rotpunkt/kueche-07.webp' => 'Küche 07', + 'marken/rotpunkt/kueche-08.webp' => 'Küche 08', + 'marken/rotpunkt/kueche-09.webp' => 'Küche 09', + 'marken/rotpunkt/kueche-10.webp' => 'Küche 10', + 'marken/rotpunkt/kueche-11.webp' => 'Küche 11', + 'marken/rotpunkt/kueche-12.webp' => 'Küche 12', + ], + ], + // Bildunterschriften nach den Febrü-Produktlinien aus den Dateinamen. + 'februe' => [ + 'image' => 'marken/februe/base-thekenloesung.webp', + 'gallery' => [ + 'marken/februe/active-2-0-bureo.webp' => 'Active 2.0 mit Bureo', + 'marken/februe/active-2-0-soul-grossraum.webp' => 'Großraum mit Active 2.0 und Soul', + 'marken/februe/adesso.webp' => 'Adesso', + 'marken/februe/arena-stage.webp' => 'Arena Stage', + 'marken/februe/base.webp' => 'Base', + 'marken/februe/base-sideboard.webp' => 'Base Sideboard', + 'marken/februe/bobb-bionics-coworker-container-cube.webp' => 'Bobb, Bionics, CoWorker, Container und Cube', + 'marken/februe/bobb.webp' => 'Bobb', + 'marken/februe/brandon.webp' => 'Brandon', + 'marken/februe/coworker.webp' => 'CoWorker', + 'marken/februe/corporate.webp' => 'Corporate', + 'marken/februe/curved-ben-basic.webp' => 'Curved, Ben und Basic', + 'marken/februe/eline-one-silent-garderobenpaneel.webp' => 'E-Line, One, Silent und Garderobenpaneel', + 'marken/februe/elements-soul-grossraum.webp' => 'Großraum mit Elements und Soul', + 'marken/februe/vio.webp' => 'Vio', + ], + ], + // Otterpohl (Zulieferung 24.09.): Titelbild und 2–5 aus der Werkstatt, der Rest sind Referenzen. + // Unterschriften ohne Kundennamen – die stehen nur in den Dateinamen der Zulieferung. + 'tischlerei' => [ + 'image' => 'marken/otterpohl/werkstatt-titel.webp', + 'gallery' => [ + 'marken/otterpohl/referenz-luecke-01.webp' => ['caption' => 'Treppe mit Lamellenwand, Sitzbank und Sideboard', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-dziuballe-treppe.webp' => ['caption' => 'Treppe mit Holzstufen, Stufenbeleuchtung und Eichenhandlauf', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-luecke-12.webp' => ['caption' => 'Tür und Kommode vor Lamellenwand', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-neugebauer.webp' => ['caption' => 'Gewendelte Treppe mit Eichenstufen', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-luecke-15.webp' => ['caption' => 'Beleuchtete Wandregale über Eck', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-berkemeier-treppe.webp' => ['caption' => 'Treppe mit umlaufender Sockelleiste und Lichtdetail', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-objektbau-luecke-technik.webp' => ['caption' => 'Objektbau: Verkaufsraum mit Theke und Regalwand', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/referenz-otterpohl-11.webp' => ['caption' => 'Sideboard mit Eichenplatte im Büro', 'category' => CmsBrand::GALLERY_PROJECT], + 'marken/otterpohl/werkstatt-02.webp' => ['caption' => 'CNC-Bearbeitungszentrum in der Fertigung', 'category' => CmsBrand::GALLERY_COMPANY], + 'marken/otterpohl/werkstatt-03.webp' => ['caption' => 'Einbau einer Tür vor Ort', 'category' => CmsBrand::GALLERY_COMPANY], + 'marken/otterpohl/werkstatt-04.webp' => ['caption' => 'Handwerkzeug in der Werkstatt', 'category' => CmsBrand::GALLERY_COMPANY], + 'marken/otterpohl/werkstatt-05-lift.webp' => ['caption' => 'Anlieferung per Möbellift – Montage inklusive', 'category' => CmsBrand::GALLERY_COMPANY], + ], + ], + ]; + + public function run(): void + { + foreach (self::IMAGES as $slug => $data) { + $brand = CmsBrand::query()->where('slug', $slug)->first(); + + if (! $brand) { + $this->command?->warn("Marke „{$slug}“ nicht gefunden – übersprungen."); + + continue; + } + + $titleSet = false; + + if (trim((string) $brand->image) === '') { + $brand->image = $data['image']; + $titleSet = true; + } + + $gallery = CmsBrand::normalizeGallery($brand->gallery); + $known = array_column($gallery, 'image'); + $added = 0; + + foreach ($data['gallery'] as $image => $entry) { + if (in_array($image, $known, true)) { + continue; + } + + $gallery[] = [ + 'image' => $image, + 'category' => is_array($entry) ? $entry['category'] : CmsBrand::GALLERY_INSPIRATION, + 'caption' => is_array($entry) ? $entry['caption'] : $entry, + ]; + $added++; + } + + $brand->gallery = $gallery; + $brand->save(); + + $this->command?->info(sprintf( + 'Marke „%s“: Titelbild %s, %d Galeriebilder ergänzt, %d vorhanden.', + $slug, + $titleSet ? 'gesetzt' : 'behalten', + $added, + count($gallery) - $added, + )); + } + } +} diff --git a/database/seeders/BrandLogoSeeder.php b/database/seeders/BrandLogoSeeder.php new file mode 100644 index 00000000..a6ba59af --- /dev/null +++ b/database/seeders/BrandLogoSeeder.php @@ -0,0 +1,72 @@ + */ + private const LOGOS = [ + 'cabinet' => 'marken/cabinet.svg', + 'rotpunkt' => 'marken/rotpunkt.svg', + 'februe' => 'marken/februe.png', + 'tischlerei' => 'marken/otterpohl.png', + ]; + + public function run(): void + { + $set = 0; + $kept = 0; + + foreach (self::LOGOS as $slug => $logo) { + $brand = CmsBrand::query()->where('slug', $slug)->first(); + + if (! $brand) { + $this->command?->warn("Marke „{$slug}“ nicht gefunden – uebersprungen."); + + continue; + } + + if (trim((string) $brand->logo) !== '') { + $kept++; + + continue; + } + + $brand->logo = $logo; + $brand->save(); + $set++; + } + + $this->command?->info("Markenlogos gesetzt: {$set}, vorhandene behalten: {$kept}."); + } +} diff --git a/database/seeders/BrandOriginSeeder.php b/database/seeders/BrandOriginSeeder.php new file mode 100644 index 00000000..5898e157 --- /dev/null +++ b/database/seeders/BrandOriginSeeder.php @@ -0,0 +1,87 @@ +, origin_distance: array, origin_role: array}> */ + private const ORIGINS = [ + 'cabinet' => [ + 'origin_city' => ['de' => 'Köln', 'en' => 'Cologne'], + 'origin_distance' => ['de' => 'rund 200 km', 'en' => 'about 200 km'], + 'origin_role' => [ + 'de' => 'Gefertigt in der Kölner Manufaktur, beraten und geplant im CABINET Store Bielefeld.', + 'en' => 'Made in the Cologne manufactory, advised and planned at the CABINET Store Bielefeld.', + ], + ], + 'rotpunkt' => [ + 'origin_city' => ['de' => 'Bünde', 'en' => 'Bünde'], + 'origin_distance' => ['de' => 'rund 30 km', 'en' => 'about 30 km'], + 'origin_role' => [ + 'de' => 'Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio.', + 'en' => 'Kitchens from their own production in Bünde – planned with us in the kitchen studio.', + ], + ], + 'februe' => [ + 'origin_city' => ['de' => 'Herford', 'en' => 'Herford'], + 'origin_distance' => ['de' => 'rund 20 km', 'en' => 'about 20 km'], + 'origin_role' => [ + 'de' => 'Büro- und Objektmöbel aus eigener Fertigung in Herford.', + 'en' => 'Office and contract furniture from their own production in Herford.', + ], + ], + 'tischlerei' => [ + 'origin_city' => ['de' => 'Rheda-Wiedenbrück', 'en' => 'Rheda-Wiedenbrück'], + 'origin_distance' => ['de' => 'rund 35 km', 'en' => 'about 35 km'], + 'origin_role' => [ + 'de' => 'Tischlerei und Innenausbau aus Rheda-Wiedenbrück – vom einzelnen Möbel bis zum ganzen Raum, inklusive Montage.', + 'en' => 'Joinery and interior fit-out from Rheda-Wiedenbrück – from a single piece to the whole room, installation included.', + ], + ], + ]; + + public function run(): void + { + $set = 0; + $kept = 0; + + foreach (self::ORIGINS as $slug => $origin) { + $brand = CmsBrand::query()->where('slug', $slug)->first(); + + if (! $brand) { + $this->command?->warn("Marke „{$slug}“ nicht gefunden – übersprungen."); + + continue; + } + + if (trim((string) $brand->getTranslation('origin_city', 'de', false)) !== '') { + $kept++; + + continue; + } + + $brand->fill($origin); + $brand->save(); + $set++; + } + + $this->command?->info("Fertigungsorte gesetzt: {$set}, vorhandene behalten: {$kept}."); + } +} diff --git a/database/seeders/BrandPageTextSeeder.php b/database/seeders/BrandPageTextSeeder.php new file mode 100644 index 00000000..088b35a1 --- /dev/null +++ b/database/seeders/BrandPageTextSeeder.php @@ -0,0 +1,438 @@ +pages() as $slug => $data) { + $brand = CmsBrand::query()->where('slug', $slug)->first(); + + if (! $brand) { + $this->command?->warn("Marke „{$slug}“ nicht gefunden – übersprungen."); + + continue; + } + + foreach (self::FIELDS as $field) { + if (array_key_exists($field, $data)) { + $brand->{$field} = $data[$field]; + } + } + + $brand->save(); + $updated++; + + $this->command?->info("Markenseite „{$slug}“: ".count($data['sections'] ?? []).' Abschnitte übernommen.'); + } + + $this->command?->info("Markenseiten aktualisiert: {$updated}."); + } + + /** + * @return array> + */ + private function pages(): array + { + return [ + 'cabinet' => [ + 'widget_label' => ['de' => 'Persönliche Schrankplanung vereinbaren', 'en' => 'Arrange a personal wardrobe consultation'], + 'description' => [ + 'de' => '

Ein Schrank ist selten der Grund, warum jemand ein Zuhause liebt. Er ist aber sehr oft der Grund, warum ein Raum funktioniert – oder eben nicht. Und Hand aufs Herz: Die schönste Dachschräge nützt wenig, wenn darunter seit dem Einzug drei Kartons stehen, für die nie ein Platz gefunden wurde.

' + .'

CABINET plant Einbauschränke, die sich dem Raum anpassen statt umgekehrt: von Wand zu Wand, vom Boden bis zur Decke, um die Schräge herum und am Heizkörper vorbei.

' + .'

Unsere Aufgabe ist es herauszufinden, was genau dieser eine Raum leisten muss. Bei uns in der Bielefelder Rathausstraße entsteht daraus nicht einfach ein Schrank, sondern eine Lösung, die den vorhandenen Platz vollständig nutzt.

', + 'en' => '

A wardrobe is rarely the reason someone loves a home. It is very often the reason a room works – or does not.

', + ], + 'cta_title' => ['de' => 'Ihr Raum. Persönlich geplant.', 'en' => 'Your room. Personally planned.'], + 'cta_text' => [ + 'de' => 'Jeder Raum hat seine Ecken, Schrägen und Eigenheiten. Gemeinsam planen wir eine CABINET-Lösung, die genau dort hineinpasst, im Alltag funktioniert und den vorhandenen Platz vollständig nutzt. Persönlich geplant bei B2in in der Rathausstraße 11 in Bielefeld.', + 'en' => 'Every room has its corners, slopes and quirks. Together we plan a CABINET solution that fits exactly, works day to day and uses the available space completely.', + ], + 'sections' => [ + [ + 'title' => 'Millimeter statt Standardmaß', + 'paragraphs' => [ + 'Schränke von der Stange sind für den durchschnittlichen Raum gemacht. Nur wohnt niemand im durchschnittlichen Raum. Es gibt die Schräge, den Mauervorsprung, das Fenster an der ungünstigen Stelle und den Heizkörper, den niemand versetzen möchte.', + 'CABINET plant jedes Element für seinen Platz. Der Schrank endet damit nicht dort, wo das Raster aufhört, sondern dort, wo der Raum aufhört. Was sonst als schwer nutzbare Restfläche übrig bleibt, wird zu Stauraum.', + ], + 'detail' => [ + 'title' => 'Wo Standardlösungen an ihre Grenzen kommen', + 'paragraphs' => [ + 'Planbar sind beispielsweise Schränke unter Dachschrägen, raumhohe Fronten bis unter die Decke, passgenaue Nischenlösungen zwischen zwei Wänden, Übergänge zu vorhandenen Möbeln sowie Aussparungen für Fensterbänke, Vorsprünge und Installationen.', + 'Auch der Sockel, die Fußleiste und der Abschluss zur Decke gehören zur Planung. Genau an diesen Übergängen entscheidet sich, ob ein Schrank wie ein Möbelstück vor der Wand wirkt oder wie ein selbstverständlicher Teil des Raumes.', + ], + ], + ], + [ + 'title' => 'Ein Bild und ein grobes Maß reichen für den Anfang', + 'paragraphs' => [ + 'Für den Anfang reicht uns ein Bild und ein grobes Maß völlig aus.', + 'Sie müssen also nicht erst alles millimetergenau ausmessen. Ein paar Fotos von der Raumsituation und die ungefähren Maße reichen, damit wir schon eine gute Planung machen und Ihnen ein konkretes Angebot erstellen können.', + 'Danach gehen wir gemeinsam ins Detail: Aufteilung, Ausstattung, Materialien, Farben und natürlich der Preis.', + 'Wenn alles grundsätzlich passt, kommen wir zum professionellen Aufmaß bei Ihnen vor Ort. Danach machen wir die Feinplanung fertig und geben den Schrank zur Bestellung frei.', + ], + 'claim' => 'So läuft’s: Foto + grobe Maße → Beratung → Planung & Angebot → Aufmaß vor Ort → Feinplanung → Bestellung', + ], + [ + 'title' => 'Innen entscheidet sich, ob ein Schrank funktioniert', + 'paragraphs' => [ + 'Eine schöne Front ist schnell ausgesucht. Ob ein Schrank im Alltag trägt, entscheidet sich dahinter: bei Einteilung, Auszügen, Fachhöhen, Beleuchtung und der Frage, was in welcher Höhe erreichbar sein muss.', + 'Wir planen die Innenausstattung deshalb nicht nach Katalogseite, sondern nach dem, was tatsächlich untergebracht werden soll.', + ], + 'detail' => [ + 'title' => 'Die Fragen, die wir vorher stellen', + 'paragraphs' => [ + 'Wie viel Hängefläche wird wirklich gebraucht und wie viel Fachboden? Wo sollen Schuhe, Koffer, Bettwäsche, Sportsachen oder Weihnachtsdeko hin? Wer nutzt den Schrank – und wie groß ist diese Person?', + 'Auch die Dinge, die man nicht jeden Tag braucht, gehören in die Planung: Wäschekörbe, Bügelbrett, Staubsauger, Karnevalskostüme oder besondere Sportsachen. Wenn wir vorher wissen, was untergebracht werden soll, können wir den Platz direkt sinnvoll dafür einplanen.', + ], + ], + ], + [ + 'title' => 'Fronten, die zum Raum gehören', + 'paragraphs' => [ + 'Ein Einbauschrank kann bewusst auffallen oder nahezu verschwinden. Fronten in Farbe, Holzoptik, Glas oder Spiegel, mit Griff oder grifflos, verändern die Wirkung eines Raumes erheblich – gerade weil ein raumhoher Schrank eine große Fläche ist.', + 'Wir stimmen Fronten, Griffe und Oberflächen auf die vorhandene Architektur ab: auf Bodenbelag, Türen, Licht und angrenzende Möbel. So entsteht kein Fremdkörper, sondern eine ruhige Fläche, die den Raum größer wirken lässt.', + ], + 'detail' => [ + 'title' => 'Sichtbar oder unsichtbar – beides ist eine Entscheidung', + 'paragraphs' => [ + 'Eine wandfarbene, grifflose Front tritt zurück und lässt den Raum offener wirken. Eine Spiegelfront bringt Licht in schmale Flure und Ankleiden. Eine dunkle Holzfront setzt dagegen bewusst einen gestalterischen Schwerpunkt.', + 'Beides ist richtig – nur eben nicht gleichzeitig. Wir klären deshalb früh, welche Rolle der Schrank im Raum übernehmen soll, bevor wir über Dekore sprechen.', + ], + ], + ], + [ + 'title' => 'Mehr als Schlafzimmer', + 'paragraphs' => [ + 'Maßgefertigter Stauraum ist längst nicht auf die Ankleide beschränkt. Genauso gefragt sind Garderoben im Flur, Hauswirtschaftsräume, Dachgeschosse, Arbeitszimmer, Wohnwände mit Medientechnik und Aktenschränke in Praxen und Kanzleien.', + 'Materialien, Farben und Gestaltungselemente lassen sich über mehrere Räume hinweg aufgreifen. So entsteht nicht in jedem Zimmer eine neue Idee, sondern ein zusammenhängendes Interior-Konzept mit einer gemeinsamen gestalterischen Sprache.', + 'Zusammen mit Rotpunkt Küchen, Febrü und weiteren ausgewählten Partnern betrachten wir bei Bedarf den gesamten Innenraum: CABINET übernimmt die millimetergenaue Sonderlösung, Rotpunkt die Küche und Febrü die Arbeitswelt.', + ], + 'claim' => 'CABINET für den Millimeter. B2in für das Ganze.', + ], + [ + 'title' => 'Warum CABINET bei uns?', + 'paragraphs' => [ + 'Bei uns beginnt eine Planung nicht mit einem Prospekt, sondern mit Ihrem Raum und der Frage, wie Sie darin leben möchten. Erst danach sprechen wir über Fronten, Auszüge und Beschläge.', + 'Wir planen Stauraum, Wege, Licht, Materialien und angrenzende Wohnbereiche als zusammenhängendes Konzept – und begleiten den gesamten Weg von der ersten Idee über das Aufmaß bis zur fertigen Montage.', + 'Dabei planen wir nicht für den nächsten schnellen Verkauf, sondern für eine möglichst lange und zufriedene Nutzung. Ein Schrank, der zu viel kann, aber nicht gebraucht wird, ist keine gute Lösung – nur eine teure.', + ], + ], + [ + 'title' => 'Erleben statt nur beschreiben', + 'paragraphs' => [ + 'Oberflächen, Beschläge und Auszüge lassen sich schlecht beschreiben und gut ausprobieren. In unserem Showroom in Bielefeld können Sie Fronten anfassen, Auszüge öffnen, Innenausstattungen vergleichen und die Unterschiede selbst beurteilen.', + 'Sie müssen sich also nicht allein auf unser Verkaufsgespräch verlassen, sondern entscheiden auf Grundlage dessen, was Sie tatsächlich gesehen und angefasst haben.', + ], + ], + ], + ], + 'rotpunkt' => [ + 'tagline' => ['de' => 'Küchen, die Haltung zeigen.', 'en' => 'Kitchens with character.'], + 'widget_label' => ['de' => 'Persönliche Küchenberatung vereinbaren', 'en' => 'Arrange a personal kitchen consultation'], + 'description' => [ + 'de' => '

Eine Küche ist weit mehr als ein funktionaler Raum. Sie ist Treffpunkt, Arbeitsplatz und Mittelpunkt des täglichen Lebens. Und Hand aufs Herz: Wo finden die besten Gespräche statt – und wo enden die meisten Partys? Meistens in der Küche.

' + .'

Rotpunkt verbindet außergewöhnliche Gestaltungsfreiheit, intelligente Stauraumlösungen und durchdachte Materiallösungen zu Küchen, die nicht nur gut aussehen, sondern dauerhaft zum Leben ihrer Besitzer passen.

' + .'

Unsere Aufgabe ist es, genau herauszuarbeiten, welche Lösung zu Ihnen, Ihrem Alltag und Ihren persönlichen Vorstellungen passt. Bei uns in der Bielefelder Rathausstraße entsteht daraus nicht einfach nur eine Küche. Auf Wunsch denken wir das ganze Zuhause mit und ergänzen es bei Bedarf bis ins Detail handwerklich.

', + 'en' => '

A kitchen is far more than a functional room. It is a meeting place, a workspace and the centre of daily life.

', + ], + 'cta_title' => ['de' => 'Ihre Küche. Persönlich geplant.', 'en' => 'Your kitchen. Personally planned.'], + 'cta_text' => [ + 'de' => 'Jeder Raum ist anders. Jeder Alltag stellt andere Anforderungen. Gemeinsam entwickeln wir eine Rotpunkt-Küche, die gestalterisch überzeugt, im Alltag funktioniert und dauerhaft zu Ihnen passt. Persönlich geplant bei B2in in der Rathausstraße 11 in Bielefeld.', + 'en' => 'Every room is different. Together we develop a Rotpunkt kitchen that convinces in design, works day to day and suits you for the long run.', + ], + 'sections' => [ + [ + 'title' => 'Aus Ostwestfalen. Für Ihr Zuhause.', + 'paragraphs' => [ + 'Rotpunkt entwickelt und fertigt seit 1930 hochwertige Küchenmöbel an den ostwestfälischen Standorten Bünde und Preußisch Oldendorf-Getmold.', + 'Regionale Fertigung, jahrzehntelange Erfahrung und die unmittelbare Nähe zum Hersteller sind für uns keine romantischen Randnotizen, sondern die Grundlage für verlässliche Qualität und persönliche Küchenplanung.', + ], + ], + [ + 'title' => 'Gestaltungsfreiheit bis ins Detail', + 'paragraphs' => [ + 'Rotpunkt bietet eine außergewöhnliche Vielfalt an Fronten, Farben, Materialien, Korpusdekoren, Arbeitsplatten, Griffvarianten und grifflosen Lösungen. Mehr als 20 beidseitig ausgeführte Korpusfarben sorgen dafür, dass eine Küche nicht nur außen, sondern auch innen konsequent gestaltet werden kann.', + 'So planen wir keinen vorgegebenen Küchenblock, sondern eine Lösung, die sich an Raum, Architektur und den tatsächlichen Gewohnheiten ihrer Besitzer orientiert.', + ], + 'detail' => [ + 'title' => 'FENIX und authentische Holzoptiken', + 'paragraphs' => [ + 'FENIX ist eine besonders matte Oberfläche mit geringer Lichtreflexion, angenehm weicher Haptik und ausgeprägten Anti-Fingerprint-Eigenschaften. Oberflächliche Mikrokratzer können thermisch behandelt werden. 27 aufeinander abgestimmte Farben eröffnen eine breite Gestaltungsvielfalt.', + 'Auch bei ausgewählten Fronten in Holzoptik zeigt sich die Liebe zum Detail. Synchronporen-Dekore stimmen sichtbare Maserung und fühlbare Struktur präzise aufeinander ab. Dadurch entsteht bereits bei pflegeleichten Melamin-Holzreproduktionen eine natürliche Wirkung.', + 'Durch eine sorgfältige Planung kann die Holzoptik über mehrere Fronten hinweggeführt werden. Das Ergebnis ist ein ruhiges, zusammenhängendes Frontbild – ohne dass dafür zwingend Echtholz eingesetzt werden muss.', + ], + ], + ], + [ + 'title' => 'Mehr Raum. Mehr Ergonomie. Mehr Küche.', + 'paragraphs' => [ + 'Besonders hohe Unterschränke, niedrige Sockelhöhen und zusätzliche Schrankbreiten schaffen mehr nutzbaren Stauraum und eröffnen neue planerische Möglichkeiten. Klingt nach Kleinkram? Vielleicht. Im täglichen Gebrauch macht genau dieser Kleinkram einen erstaunlich großen Unterschied.', + ], + 'detail' => [ + 'title' => 'NEW DIMENSIONS', + 'paragraphs' => [ + 'Mit dem Maßkonzept NEW DIMENSIONS sind beispielsweise Unterschrankhöhen von bis zu 910 Millimetern in Verbindung mit einem nur 50 Millimeter hohen Sockel möglich.', + 'Zusätzliche Schrankbreiten und vielfältige Innenausstattungen helfen uns, den vorhandenen Raum konsequent und ergonomisch zu nutzen. Die Küche wirkt dadurch nicht nur großzügiger – sie ist es tatsächlich.', + ], + ], + ], + [ + 'title' => 'Nachhaltigkeit mit konkreter Substanz', + 'paragraphs' => [ + 'Nachhaltigkeit braucht keine großen Versprechen, sondern nachvollziehbare Fakten. Greenline steht bei Rotpunkt für konkrete Materialien, Zertifizierungen und messbare Unterschiede. Deshalb legen wir offen, welche Materialien Rotpunkt verwendet, welche Zertifizierungen dahinterstehen und welchen konkreten Unterschied Greenline für Umwelt und Raumklima macht.', + 'Die Greenline BioBoard Gen2 besteht aus bis zu 90 Prozent recyceltem Holz. 95 Prozent des Rotpunkt-Küchensortiments sind FSC-zertifiziert. Außerdem sind die Produkte nach JIS F4Star hinsichtlich ihrer Formaldehydemissionen zertifiziert.', + 'Sie haben bereits eine vergleichbare Küche im Blick, die beim Thema Nachhaltigkeit mehr verspricht? Bringen Sie die Unterlagen gerne mit. Wir schauen gemeinsam genau hin – ehrlich, nachvollziehbar und ohne Verkaufsnebel.', + ], + 'detail' => [ + 'title' => 'Zahlen, Zertifizierungen und Raumklima', + 'paragraphs' => [ + // ⚠️ Waldflächen-Aussage: laut Zulieferung nur mit ausdrücklicher Herstellerbestätigung + // veröffentlichen. Steht als Punkt in der Deployment-Checkliste. + 'Nach Angaben von Rotpunkt bleibt durch den Einsatz der Greenline-Spanplatten jährlich eine Waldfläche von ungefähr 50 Fußballfeldern erhalten.', + 'Die Greenline BioBoard Gen2 gibt laut Rotpunkt rund 50 Prozent weniger Formaldehyd an die Raumluft ab als herkömmliche Produkte. Das ist insbesondere für ein möglichst gesundes Raumklima relevant.', + 'Die FSC-Zertifizierung wird unter der Kennung FSC C131864 geführt. Entscheidend ist für uns nicht das grüne Etikett, sondern die konkrete und überprüfbare Aussage dahinter.', + ], + ], + ], + [ + 'title' => 'Mehr als Küchen', + 'paragraphs' => [ + 'Die Möglichkeiten von Rotpunkt enden nicht bei der klassischen Küche. Das Möbel- und Planungssystem lässt sich auch für Hauswirtschaftsräume, Badezimmer, Sideboards, Wohnmöbel und weitere Bereiche rund um das Wohnen einsetzen.', + 'Materialien, Farben und Gestaltungselemente können dabei über mehrere Räume hinweg aufgegriffen werden. So entsteht ein stimmiges Gesamtbild, statt in jedem Raum wieder ganz von vorne anzufangen.', + 'Bei Bedarf denken wir den Innenraum deshalb auch über die Küche hinaus – von Möbeln und Beleuchtung bis zu Tapeten, Fußböden und, wenn es sein soll, der letzten Blume.', + ], + ], + [ + 'title' => 'Warum eine Rotpunkt-Küche bei uns?', + 'paragraphs' => [ + 'Bei uns beginnt eine neue Küche nicht mit einem Prospekt, sondern mit Ihrem Raum und der Frage, wie Sie darin leben möchten.', + 'Arbeitsabläufe, Ergonomie, Stauraum, Materialien, Beleuchtung, Geräte und angrenzende Wohnbereiche planen wir als zusammenhängendes Konzept. Auch die Partystellplätze wollen wohlüberlegt sein – also jene Orte, an denen Gäste später stehen, sitzen, reden und vielleicht etwas länger bleiben.', + 'Wir stellen nicht einfach möglichst viele Produkte zusammen. Wir entwickeln eine Küche, die gestalterisch, funktional und wirtschaftlich überzeugt – und begleiten den gesamten Weg von der ersten Idee bis zur professionellen Montage.', + 'Dabei planen wir nicht für den nächsten schnellen Verkauf, sondern für eine möglichst lange und zufriedene Nutzung. Das ist kurzfristig vielleicht nicht immer die wirtschaftlichste Entscheidung für uns. Für eine ehrliche und dauerhafte Kundenbeziehung ist es die einzig richtige.', + ], + ], + [ + 'title' => 'Erleben statt nur erzählen', + 'paragraphs' => [ + 'Eine gute Entscheidung entsteht nicht allein am Planungstisch. Auf Wunsch – und manchmal auch mit etwas freundlicher Überzeugungsarbeit – begleiten wir Sie nach Abstimmung in das nahe Rotpunkt-Werk oder in ausgewählte Ausstellungen der Gerätehersteller.', + 'Dort können Sie Materialien, Fertigung und Technik selbst erleben, Geräte ausprobieren und Fragen direkt vor Ort stellen. Sie müssen sich also nicht allein auf unser Verkaufsgespräch verlassen, sondern erhalten Informationen aus erster Hand, können vergleichen und anschließend mit einem sicheren Gefühl entscheiden.', + 'Wir begleiten Sie dabei, ordnen die unterschiedlichen Möglichkeiten ein und führen Ihre Entscheidungen zu einem stimmigen Gesamtkonzept zusammen.', + ], + ], + ], + ], + 'februe' => [ + 'tagline' => ['de' => 'Arbeiten, wo man gerne bleibt.', 'en' => 'Work where people like to stay.'], + 'widget_label' => ['de' => 'Persönliche Arbeitsplatzberatung vereinbaren', 'en' => 'Arrange a personal workspace consultation'], + 'description' => [ + 'de' => '

Ein Büro ist kein Parkplatz für Schreibtische. Es ist ein Raum für Konzentration, Austausch, Bewegung und neue Ideen. Und manchmal entsteht die beste Idee eben nicht am Schreibtisch, sondern zwischen Kaffeemaschine, Lounge und einem guten Gespräch.

' + .'

Febrü verbindet ergonomische Arbeitsplätze mit wohnlichem Design, intelligentem Stauraum, durchdachter Akustik und flexiblen Raumstrukturen.

' + .'

Wir entwickeln daraus Arbeitswelten, die zu den Menschen, ihren Aufgaben und ihrer Umgebung passen – vom einzelnen Homeoffice bis zur vollständigen Unternehmensfläche.

', + 'en' => '

An office is not a car park for desks. It is a space for concentration, exchange, movement and new ideas.

', + ], + 'cta_title' => ['de' => 'Ihr Arbeitsplatz. Persönlich geplant.', 'en' => 'Your workspace. Personally planned.'], + 'cta_text' => [ + 'de' => 'Ob Homeoffice, Praxis, Kanzlei, wachsendes Unternehmen oder vollständige Arbeitswelt: Wir planen jeden Arbeitsplatz mit demselben Anspruch. Nur der Umfang verändert sich. Gemeinsam entwickeln wir eine Arbeitsumgebung, die gesundes Arbeiten unterstützt, gestalterisch überzeugt und den Menschen zeigt, dass ihre tägliche Leistung wahrgenommen und wertgeschätzt wird – persönlich geplant bei B2in in der Rathausstraße 11 in Bielefeld.', + 'en' => 'Whether a home office, a practice, a law firm or a complete working environment: we plan every workspace to the same standard. Only the scope changes.', + ], + 'sections' => [ + [ + 'title' => 'Zuhause oder Projekt. Zwei Wege. Derselbe Anspruch.', + 'paragraphs' => [ + 'Unsere Startseite führt bewusst über zwei Wege: Zuhause und Projekt. Denn ein einzelnes Homeoffice stellt andere Anforderungen als eine Büroetage mit fünfzig Arbeitsplätzen.', + 'Unterschiedliche Aufgaben brauchen unterschiedliche Planung und natürlich auch unterschiedliche Kalkulationen. Bei uns gibt es trotzdem nur Kunden – mit unterschiedlichen Räumen, Anforderungen und Stückzahlen. Der Anspruch an Produkt, Beratung und Service bleibt derselbe.', + ], + 'detail' => [ + 'title' => 'Bei uns gibt es Kunden – keine Schubladen', + 'paragraphs' => [ + 'Ob Privatkunde oder Unternehmen: Für uns gibt es zunächst einmal Kunden mit unterschiedlichen Räumen, Anforderungen und Stückzahlen.', + 'Natürlich beeinflusst die Menge den Preis. Fünfzig Arbeitsplätze werden anders kalkuliert als ein einzelner Schreibtisch fürs Homeoffice. Der Anspruch an Produkt, Beratung und Service bleibt trotzdem derselbe.', + 'Febrü bleibt Febrü. B2in bleibt B2in. Und jeder Kunde erhält eine Lösung, die zu seinem tatsächlichen Bedarf passt.', + ], + ], + 'claim' => 'Ob im Kleinen oder im Großen – die Detailtreue entscheidet.', + ], + [ + 'title' => 'Aus Herford. Für moderne Arbeitswelten.', + 'paragraphs' => [ + 'Febrü entwickelt und produziert hochwertige Büromöbel direkt im ostwestfälischen Herford – nur wenige Kilometer von Bielefeld entfernt. Eine sehr hohe Fertigungstiefe ermöglicht kurze Wege, verlässliche Qualität und hohe Flexibilität bei Materialien, Farben und individuellen Anforderungen.', + ], + 'detail' => [ + 'title' => 'Von einem Schreibtisch bis zum vollständigen Bürotrakt', + 'paragraphs' => [ + 'Das Sortiment reicht von ergonomischen und höhenverstellbaren Schreibtischen über Konferenz- und Besprechungstische, Bürostühle und Loungeelemente bis zu Empfangslösungen, Stauraum, Akustikelementen, mobilen Medienmöbeln, Schließfachsystemen und flexiblen Raumstrukturen.', + 'Damit lässt sich ein einzelner Arbeitsplatz genauso überzeugend planen wie eine Praxis, eine Kanzlei, ein Konferenzbereich oder ein vollständiger Bürotrakt.', + ], + ], + 'claim' => 'Made in Herford. Geplant in Bielefeld.', + ], + [ + 'title' => 'Nicht drei Marken. Ein durchgängiger Raum.', + 'paragraphs' => [ + 'Ein Bürotrakt endet nicht am Schreibtisch. Menschen brauchen Orte zum Arbeiten, für Besprechungen und Rückzug – aber ebenso für Pausen, den gemeinsamen Kaffee, das Mittagessen und den informellen Austausch.', + 'Genau hier greifen Febrü, Rotpunkt und CABINET bei uns ineinander: Febrü gestaltet die Arbeitswelt, Rotpunkt die Küche im Bürotrakt und CABINET die millimetergenaue Sonderlösung.', + 'Entscheidend ist nicht, welcher Hersteller welches Möbel liefert. Entscheidend ist, dass am Ende alles zusammengehört. Wir stimmen Arbeitsplätze, Küche, Stauraum, Farben, Materialien, Beleuchtung und Akustik zu einem durchgängigen Konzept aufeinander ab.', + ], + 'detail' => [ + 'title' => 'Wie die drei Marken zusammenspielen', + 'paragraphs' => [ + 'Febrü gestaltet ergonomische Arbeitsplätze, Konferenzbereiche, Loungezonen, Akustik, Stauraum und flexible Raumstrukturen.', + 'Rotpunkt gestaltet Mitarbeiterküchen, Coffee-Points und gemeinsam genutzte Aufenthaltsküchen als selbstverständlichen Bestandteil der Arbeitswelt – und nicht als übrig gebliebene Funktionszeile. Gerade hier entstehen oft Gespräche, Verbindungen und Ideen, für die im offiziellen Meeting kein Platz war.', + 'CABINET übernimmt dort, wo Nischen vollständig genutzt, Schränke von Wand zu Wand und vom Boden bis zur Decke geplant oder besondere Übergänge zwischen Arbeitsplatz, Stauraum und Architektur geschaffen werden sollen. So entstehen beispielsweise maßgefertigte Garderoben, Aktenschränke, Medienlösungen und Stauraumelemente.', + ], + ], + 'claim' => 'Febrü für die Arbeitswelt. Rotpunkt für die Küche. CABINET für die millimetergenaue Sonderlösung. B2in für das Ganze.', + ], + [ + 'title' => 'Ein Arbeitsplatz. Fünfzig Arbeitsplätze. Ein Anspruch.', + 'paragraphs' => [ + 'Ob wir über einen einzelnen Arbeitsplatz oder einen vollständigen Bürotrakt sprechen: Das grundsätzliche Problem bleibt dasselbe. Fühlt sich der Mensch, der dort täglich arbeitet, gesehen und wertgeschätzt – oder wurde ihm lediglich irgendwo ein Tisch hingestellt?', + 'Die Stückzahl verändert den Umfang der Aufgabe, aber nicht ihre Bedeutung. Wir bewerten einen Arbeitsplatz deshalb nicht nach Quadratmetern oder Auftragswert, sondern danach, was er für den Menschen leisten muss.', + ], + 'detail' => [ + 'title' => 'Wertschätzung ist keine Frage der Stückzahl', + 'paragraphs' => [ + 'Gerade in Zeiten des Fachkräftemangels können Unternehmen es sich nicht leisten, Arbeitsbedingungen als Nebensache zu behandeln. Wer Top-Leistung erwartet, muss die passenden Voraussetzungen schaffen. Ein Obstkorb und kostenloses Wasser sind freundlich – ersetzen aber keinen guten Arbeitsplatz.', + 'Besonders deutlich wird das bei Menschen, die Beruf und Familie miteinander organisieren. Teilzeit bedeutet nicht halbe Leistung. Oft wird in einem kürzeren Zeitfenster hoch konzentriert gearbeitet, während gleichzeitig Schule, Termine, Betreuung und der ganz normale Familienalltag organisiert werden müssen.', + 'Gerade hier darf der Arbeitsplatz kein Provisorium aus Küchentisch, Beistelltisch oder Schlafzimmerecke sein. Er muss sofort funktionieren, Bewegung ermöglichen, technisch durchdacht sein und sich trotzdem vernünftig in das Zuhause einfügen. Für den Menschen, der ihn täglich nutzt, ist ein einzelner Arbeitsplatz keine kleine Lösung. Er ist seine vollständige Arbeitswelt.', + ], + ], + ], + [ + 'title' => 'Ergonomie, die im Alltag funktioniert', + 'paragraphs' => [ + 'Ein Arbeitsplatz muss sich dem Menschen anpassen – nicht umgekehrt. Höhenverstellbare Schreibtische und ergonomische Bürostühle schaffen Bewegung und unterstützen eine gesunde Haltung.', + 'Ergonomie endet aber nicht bei Tisch und Stuhl. Auch Laufwege, Greifräume, Bildschirmposition, Beleuchtung, Stauraum und der Wechsel zwischen Tätigkeiten bestimmen, wie angenehm und produktiv ein Arbeitstag verläuft.', + ], + 'detail' => [ + 'title' => 'Und nein: Wir meinen nicht nur den Weg zur Kaffeemaschine', + 'paragraphs' => [ + 'Sinnvolle Laufwege sind Teil einer guten Planung – auch wenn Kaffeemaschine und Kühlschrank zweifellos eine gewisse Bedeutung für den Arbeitsalltag haben.', + 'Das gilt für vier Stunden ebenso wie für zehn Stunden am Schreibtisch, im Unternehmen genauso wie im privaten Zuhause. Und natürlich müssen die Möbel gestalterisch zum Raum passen. Wer denkt: „Das sieht doch keiner – außer den Mitarbeitern“, hat den ersten entscheidenden Fehler bereits gemacht. Die Mitarbeiter sehen es. Jeden Tag. Und sie arbeiten darin.', + ], + ], + 'quote' => 'Ich bin, sagen wir, alt genug, um beim Aufstehen gelegentlich zu stöhnen. Und ehrlich gesagt fühle ich mich an manchen Tagen noch etwas älter. Mein Arzt sagt, ich solle weniger sitzen. Mein Beruf findet nun einmal zu einem großen Teil am Computer statt. Ergo brauche ich eine Lösung, die mir mehr Bewegung ermöglicht, ohne dass ich dafür meinen Beruf wechseln muss.', + 'quote_author' => 'Marcel Scheibe, Geschäftsführer von B2in', + ], + [ + 'title' => 'Office meets Interior', + 'paragraphs' => [ + 'Identische Schreibtischreihen werden einem modernen Arbeitstag nicht mehr gerecht. Menschen benötigen Bereiche für Konzentration, spontane Gespräche, Teamarbeit, Videokonferenzen, vertrauliche Besprechungen und erholsame Pausen.', + 'Febrü verbindet dafür klassische Arbeitsplatzmöbel mit wohnlichen Loungeelementen, mobilen Multifunktionslösungen, Stauraum, Akustik und flexibler Raumzonierung. So entsteht nicht für jede Person derselbe Platz, sondern für jede Tätigkeit der richtige.', + ], + 'detail' => [ + 'title' => 'Das Wir und das Ich', + 'paragraphs' => [ + 'Ja, ja – das könnte man unter dem üblichen Verkaufs-Blabla abheften. Dahinter steht aber eine einfache Wahrheit: Verantwortung lässt sich nicht dauerhaft nach außen delegieren. Was bleibt, sind das Wir und das Ich.', + 'Ein Unternehmen muss Bedingungen schaffen, unter denen Menschen gemeinsam gut arbeiten, sich austauschen und konzentrieren können. Der Einzelne erhält die Möglichkeit – und trägt auch die Verantwortung –, daraus das Beste zu machen.', + 'Räume und Möbel schaffen allein noch keine gute Unternehmenskultur. Schlechte Arbeitsbedingungen können eine gute Kultur aber jeden Tag unnötig erschweren.', + ], + ], + 'claim' => 'Das Unternehmen stärkt das Wir. Der Einzelne macht daraus das Beste.', + ], + [ + 'title' => 'Ruhe und Flexibilität sind planbar.', + 'paragraphs' => [ + 'Akustik, mobile Elemente und flexible Raumlösungen helfen dabei, Bereiche für Kommunikation, Konzentration und Rückzug zu schaffen – und sie später an veränderte Teams und Arbeitsweisen anzupassen.', + ], + 'detail' => [ + 'title' => 'Akustik und flexible Raumstrukturen', + 'paragraphs' => [ + 'Hallende Räume, Telefongespräche und Videokonferenzen können Konzentration und Sprachverständlichkeit erheblich beeinträchtigen. Akustikpaneele, Trennwände, Screens und entsprechend geplante Möbel helfen, Schall zu reduzieren.', + 'Mit modularen Möbeln und Raum-in-Raum-Lösungen lassen sich Arbeitsbereiche neu ordnen, ohne bei jeder Veränderung gleich feste Wände bauen zu müssen. Das ist im großen Büro wichtig – und im privaten Zuhause häufig noch wichtiger.', + ], + ], + ], + [ + 'title' => 'Gestaltung, die zur Identität passt', + 'paragraphs' => [ + 'Ein Büro ist ein sichtbarer Teil der Unternehmenskultur. Farben, Materialien und Raumwirkung beeinflussen, wie Mitarbeitende, Kunden und Bewerber ein Unternehmen wahrnehmen. Im Homeoffice muss der Arbeitsplatz professionell funktionieren und sich zugleich in die vorhandene Einrichtung einfügen.', + 'Dank der hohen Fertigungstiefe bietet Febrü eine große Auswahl an Materialien, Dekoren, Stoffen und Gestellfarben. Corporate Design muss nicht am Briefpapier enden. Und ein Homeoffice muss nach Feierabend nicht wie ein kleines Finanzamt aussehen.', + ], + 'detail' => [ + 'title' => 'Und ja: Pflanzen gehören dazu', + 'paragraphs' => [ + 'Eine Pflanze macht aus einem Unternehmen noch kein nachhaltiges Unternehmen. Sie macht aus einem technisch eingerichteten Raum aber häufig erst einen Ort, an dem Menschen gerne arbeiten. Das hat nichts mit Greenwashing zu tun, sondern schlicht mit Atmosphäre und Stil.', + ], + ], + ], + [ + 'title' => 'Nachhaltigkeit, die länger als bis zum nächsten Umzug hält', + 'paragraphs' => [ + 'Nachhaltigkeit beginnt bei der Lebensdauer eines Möbelstücks. Viele Febrü-Möbel sind modular aufgebaut, sodass Komponenten ausgetauscht oder ergänzt werden können, statt bei veränderten Anforderungen das gesamte Möbelstück zu ersetzen.', + ], + 'detail' => [ + 'title' => 'Konkrete Standards statt allgemeiner Versprechen', + 'paragraphs' => [ + 'Febrü produziert mit hoher Fertigungstiefe direkt in Herford. Das sorgt für kurze Wege und kontrollierte Prozesse. Mit laserTEC entstehen hochwertige, fugenlose Kanten ohne zusätzlichen Klebstoff.', + // ⚠️ Blauer Engel gilt laut Zulieferung nur für ausgewählte Tischsysteme und + // Stauraummöbel. Produktbezug beim Redigieren zwingend beibehalten. + 'Febrü arbeitet nach ISO 9001 und ISO 14001 und verfügt über einen PEFC-Produktkettennachweis. Ausgewählte Febrü-Tischsysteme und Stauraummöbel tragen den Blauen Engel.', + 'Nachhaltig ist nicht das Möbelstück mit dem längsten Werbetext. Nachhaltig ist das Möbelstück, das viele Jahre funktioniert und veränderte Anforderungen mitmacht.', + ], + ], + ], + [ + 'title' => 'Warum Febrü bei uns?', + 'paragraphs' => [ + 'Bei uns beginnt eine Planung nicht mit einem Möbelkatalog, sondern mit den Menschen und der Frage, wie tatsächlich gearbeitet wird. Wo ist Konzentration gefragt? Wo entstehen Gespräche? Welche Bereiche benötigen Diskretion? Und wie viel Stauraum wird wirklich gebraucht?', + 'Wir verbinden Ergonomie, Laufwege, Akustik, Stauraum, Beleuchtung, Kommunikation und Rückzug zu einem Gesamtkonzept. Es geht nicht darum, möglichst viele Möbel in einen Raum zu stellen. Es geht darum, eine bessere Arbeitswelt zu schaffen.', + ], + 'detail' => [ + 'title' => 'Im Homeoffice stellen sich andere Fragen', + 'paragraphs' => [ + 'Wie sichtbar darf der Arbeitsplatz sein? Wo verschwinden Technik und Unterlagen nach Feierabend? Und wie lässt sich professionelles Arbeiten so in die vorhandene Einrichtung integrieren, dass das Zuhause ein Zuhause bleibt?', + 'Auch hier gilt: Ein einzelner Arbeitsplatz ist keine kleinere Aufgabe. Er braucht nur eine andere Antwort als ein vollständiger Bürotrakt.', + ], + ], + ], + [ + 'title' => 'Erleben statt nur aus dem Katalog auswählen', + 'paragraphs' => [ + 'Durch die Nähe zwischen Bielefeld und Herford können Febrü-Produkte nach Abstimmung persönlich erlebt werden. Wir organisieren und begleiten den Besuch der Ausstellung – für einen Schreibtisch genauso wie für einen vollständigen Unternehmensstandort.', + 'Dort lassen sich Materialien anfassen, Stühle und Tische ausprobieren, Akustiklösungen erleben und unterschiedliche Arbeitswelten vergleichen. Sie müssen sich nicht allein auf ein Verkaufsgespräch verlassen.', + ], + ], + ], + ], + ]; + } +} diff --git a/database/seeders/CmsArticleSeeder.php b/database/seeders/CmsArticleSeeder.php index 5ca7bcdd..6ce7d38d 100644 --- a/database/seeders/CmsArticleSeeder.php +++ b/database/seeders/CmsArticleSeeder.php @@ -8,19 +8,6 @@ use Illuminate\Support\Facades\Lang; class CmsArticleSeeder extends Seeder { - /** - * Numerische IDs aus resources/lang/{locale}/b2in.php → Slugs für cms_articles.slug - * - * @var array - */ - private const ARTICLE_ID_TO_SLUG = [ - 1 => 'escrow-system-dubai-investoren', - 2 => 'spotlight-al-jaddaf-hotspot', - 3 => 'turnkey-investments-moeblierung-rendite', - 4 => 'supply-chain-management-vertragssicherheit', - 5 => 'local-for-local-regionaler-moebelhandel', - ]; - public function run(): void { foreach ($this->getArticles() as $i => $data) { @@ -49,19 +36,28 @@ class CmsArticleSeeder extends Seeder $articlesEn = []; } - ksort($articlesDe); - + // Kein ksort: die Reihenfolge in der Lang-Datei bestimmt die Reihenfolge im Magazin – + // im CMS ueber `order`, im Lang-Fallback ueber die Array-Reihenfolge. Beide Wege stimmen + // damit ueberein. $out = []; foreach ($articlesDe as $id => $rowDe) { - $id = (int) $id; - $slug = self::ARTICLE_ID_TO_SLUG[$id] ?? null; + if (! is_array($rowDe)) { + continue; + } + + $slug = CmsArticle::slugForLangArticle($id, $rowDe); + if ($slug === null) { continue; } $rowEn = $articlesEn[$id] ?? $rowDe; + if (! is_array($rowEn)) { + $rowEn = $rowDe; + } + $out[] = [ 'slug' => $slug, 'title' => [ @@ -74,6 +70,7 @@ class CmsArticleSeeder extends Seeder ], 'image' => (string) ($rowDe['image'] ?? ''), 'category' => (string) ($rowDe['category'] ?? ''), + 'world' => (string) ($rowDe['world'] ?? CmsArticle::WORLD_INTERIOR), 'date_label' => (string) ($rowDe['date'] ?? ''), 'read_time' => (string) ($rowDe['readTime'] ?? ''), 'author' => is_array($rowDe['author'] ?? null) ? $rowDe['author'] : [], diff --git a/database/seeders/CmsBrandSeeder.php b/database/seeders/CmsBrandSeeder.php new file mode 100644 index 00000000..d5dd459f --- /dev/null +++ b/database/seeders/CmsBrandSeeder.php @@ -0,0 +1,172 @@ +brands() as $index => $data) { + if (CmsBrand::query()->where('slug', $data['slug'])->exists()) { + continue; + } + + CmsBrand::query()->create(array_merge($data, ['order' => $index + 1])); + $created++; + } + + $this->command?->info("Kernmarken: {$created} angelegt."); + } + + /** + * @return array> + */ + private function brands(): array + { + return [ + [ + 'slug' => 'cabinet', + 'logo' => 'marken/cabinet.svg', + 'name' => ['de' => 'CABINET', 'en' => 'CABINET'], + 'tagline' => ['de' => 'Einbauschränke nach Maß.', 'en' => 'Fitted wardrobes made to measure.'], + 'teaser' => [ + 'de' => 'Maßgefertigte Schranklösungen, geplant bis auf den Millimeter – gefertigt für Räume, in denen Standard nicht genügt.', + 'en' => 'Bespoke wardrobe solutions, planned to the millimetre – built for rooms where standard will not do.', + ], + 'summary' => [ + 'de' => 'Wo Standardmaße an ihre Grenzen kommen, fängt CABINET an. Schranklösungen, die exakt in Ihren Raum geplant werden – von der Dachschräge über die Nische bis zur raumhohen Front. Gefertigt in der Qualität, die man erst beim zweiten Hinschauen ganz erfasst. + +Erleben Sie die Möglichkeiten in unserem Showroom in Bielefeld.', + 'en' => 'Where standard dimensions reach their limits, CABINET begins. Wardrobe solutions planned precisely for your room – from sloped ceilings and alcoves to full-height fronts. Built to a quality you only fully grasp on second look. + +Experience the possibilities in our Bielefeld showroom.', + ], + 'highlights' => [ + 'Millimetergenaue Planung', + 'Individuelle Fronten und Innenausstattung', + 'Persönliche Beratung vor Ort', + ], + 'description' => [ + 'de' => '

CABINET steht für Einbaulösungen, die sich dem Raum anpassen statt umgekehrt. Jedes Element wird für seinen Platz geplant – vom Dachschrägenschrank bis zur raumhohen Garderobe. Gefertigt in Deutschland, montiert von Fachleuten, die den Unterschied zwischen „passt" und „sitzt" kennen.

', + 'en' => '

CABINET stands for built-in solutions that adapt to the room rather than the other way round. Every element is planned for its place – from sloped-ceiling wardrobes to full-height hallway storage. Made in Germany, installed by specialists who know the difference between fitting and belonging.

', + ], + 'services' => [ + ['title' => 'Aufmaß vor Ort', 'description' => 'Wir messen millimetergenau in Ihren Räumen – Grundlage jeder Planung.'], + ['title' => 'Individuelle Planung', 'description' => 'Grundriss, Nutzung und Stil bestimmen die Lösung, nicht der Katalog.'], + ['title' => 'Fertigung in Deutschland', 'description' => 'Materialien und Verarbeitung, die auf Dauer angelegt sind.'], + ['title' => 'Montage durch Fachleute', 'description' => 'Wir liefern und montieren – bis zum letzten Detail.'], + ], + 'showroom' => 'Showroom Bielefeld', + 'origin_city' => ['de' => 'Köln', 'en' => 'Cologne'], + 'origin_distance' => ['de' => 'rund 200 km', 'en' => 'about 200 km'], + 'origin_role' => [ + 'de' => 'Gefertigt in der Kölner Manufaktur, beraten und geplant im CABINET Store Bielefeld.', + 'en' => 'Made in the Cologne manufactory, advised and planned at the CABINET Store Bielefeld.', + ], + 'is_published' => true, + ], + [ + 'slug' => 'rotpunkt', + 'logo' => 'marken/rotpunkt.svg', + 'name' => ['de' => 'Rotpunkt Küchen', 'en' => 'Rotpunkt Küchen'], + 'tagline' => ['de' => 'Küchen, die Haltung zeigen.', 'en' => 'Kitchens with character.'], + 'teaser' => [ + 'de' => 'Premium-Küchen aus deutscher Fertigung – individuell geplant, nachhaltig gedacht und gebaut für den Mittelpunkt Ihres Zuhauses.', + 'en' => 'Premium kitchens made in Germany – individually planned, sustainably conceived and built for the heart of your home.', + ], + 'summary' => [ + 'de' => 'Premium-Küchen aus deutscher Fertigung – individuell geplant und gebaut für den Raum, in dem am meisten passiert. Rotpunkt steht für durchdachte Funktion, langlebige Materialien und einen Anspruch an Nachhaltigkeit, der über das Marketing hinausgeht. + +Beratung und Planung in unserem Küchenstudio.', + 'en' => 'Premium kitchens made in Germany – individually planned and built for the room where most of life happens. Rotpunkt stands for considered function, durable materials and a commitment to sustainability that goes beyond marketing. + +Consultation and planning in our kitchen studio.', + ], + 'highlights' => [ + 'Fertigung in Deutschland', + 'Individuelle Planung nach Ihrem Grundriss', + 'Nachhaltige Materialien', + ], + 'description' => [ + 'de' => '

Die Küche ist der Raum, in dem sich das Leben sammelt. Rotpunkt fertigt sie in Deutschland, mit Blick auf Materialien, die halten, und auf eine Planung, die zum Alltag passt – nicht zum Prospekt. Wir begleiten von der ersten Skizze bis zur fertigen Küche.

', + 'en' => '

The kitchen is where life gathers. Rotpunkt builds them in Germany, with an eye on materials that last and on planning that fits everyday life – not a brochure. We accompany you from the first sketch to the finished kitchen.

', + ], + 'services' => [ + ['title' => 'Küchenplanung', 'description' => 'Gemeinsam entwickeln wir Grundriss, Stauraum und Arbeitswege.'], + ['title' => 'Nachhaltige Materialien', 'description' => 'Deutsche Fertigung mit Blick auf Herkunft und Langlebigkeit.'], + ['title' => 'Geräte und Technik', 'description' => 'Passende Ausstattung, abgestimmt auf Ihre Art zu kochen.'], + ['title' => 'Lieferung und Aufbau', 'description' => 'Koordiniert aus einer Hand, inklusive Anschluss.'], + ], + 'showroom' => 'Küchenstudio', + 'origin_city' => ['de' => 'Bünde', 'en' => 'Bünde'], + 'origin_distance' => ['de' => 'rund 30 km', 'en' => 'about 30 km'], + 'origin_role' => [ + 'de' => 'Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio.', + 'en' => 'Kitchens from their own production in Bünde – planned with us in the kitchen studio.', + ], + 'is_published' => true, + ], + [ + 'slug' => 'februe', + 'logo' => 'marken/februe.png', + 'name' => ['de' => 'Febrü', 'en' => 'Febrü'], + 'tagline' => ['de' => 'Arbeiten, wo man gerne bleibt.', 'en' => 'Work where you like to stay.'], + 'teaser' => [ + 'de' => 'Möbel für konzentriertes Arbeiten – vom durchdachten Home-Office bis zur kompletten Büroeinrichtung.', + 'en' => 'Furniture for focused work – from a well-considered home office to a complete office fit-out.', + ], + 'summary' => [ + 'de' => 'Ob Home-Office oder ganze Büroetage: Febrü macht Möbel für konzentriertes Arbeiten. Höhenverstellbare Tische, durchdachte Stauraumlösungen und Sitzmöbel, die auch nach acht Stunden noch überzeugen – mit einem Design, das ins Zuhause genauso passt wie ins Unternehmen.', + 'en' => 'Whether a home office or a whole office floor: Febrü makes furniture for focused work. Height-adjustable desks, well-considered storage and seating that still convinces after eight hours – with a design that suits the home as much as the company.', + ], + 'highlights' => [ + 'Ergonomie, die man merkt', + 'Vom einzelnen Arbeitsplatz bis zur Bürofläche', + 'Für Zuhause und fürs Unternehmen', + ], + 'description' => [ + 'de' => '

Gutes Arbeiten beginnt beim Möbel. Febrü baut Tische, Stühle und Stauraum, die den Rücken entlasten und den Raum ordnen – zuerst für das Zuhause, in dem immer öfter gearbeitet wird, und ebenso für Büros und Unternehmen.

', + 'en' => '

Good work begins with the furniture. Febrü builds desks, chairs and storage that relieve the back and bring order to a room – first for the home, where more and more work happens, and equally for offices and companies.

', + ], + 'services' => [ + ['title' => 'Home-Office', 'description' => 'Ein Arbeitsplatz, der sich ins Zuhause fügt statt es zu besetzen.'], + ['title' => 'Ergonomie', 'description' => 'Höhenverstellbare Tische und Sitzlösungen für langes Arbeiten.'], + ['title' => 'Büroeinrichtung', 'description' => 'Vom einzelnen Arbeitsplatz bis zur kompletten Fläche.'], + ['title' => 'Akustik und Ordnung', 'description' => 'Stauraum und Raumgliederung, die Konzentration ermöglichen.'], + ], + 'showroom' => '', + 'origin_city' => ['de' => 'Herford', 'en' => 'Herford'], + 'origin_distance' => ['de' => 'rund 20 km', 'en' => 'about 20 km'], + 'origin_role' => [ + 'de' => 'Büro- und Objektmöbel aus eigener Fertigung in Herford.', + 'en' => 'Office and contract furniture from their own production in Herford.', + ], + 'is_published' => true, + ], + // Vierte Kernmarke Otterpohl (Nachtrag 15). Slug bleibt „tischlerei“, Texte aus OtterpohlBrandSeeder. + array_merge(OtterpohlBrandSeeder::data(), [ + 'slug' => OtterpohlBrandSeeder::SLUG, + 'logo' => OtterpohlBrandSeeder::LOGO, + ]), + ]; + } +} diff --git a/database/seeders/CmsContentSeeder.php b/database/seeders/CmsContentSeeder.php index 3384386e..1403619f 100644 --- a/database/seeders/CmsContentSeeder.php +++ b/database/seeders/CmsContentSeeder.php @@ -3,11 +3,34 @@ namespace Database\Seeders; use FluxCms\Core\Models\CmsContent; +use FluxCms\Core\Services\CmsContentService; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Lang; class CmsContentSeeder extends Seeder { + /** + * Überschreibt bestehende CMS-Einträge mit den Werten aus den Lang-Dateien. + * + * Standardmäßig aus, damit ein erneuter Seeder-Lauf die im Admin gepflegten Inhalte + * nicht zurücksetzt. Nur einschalten, wenn die Lang-Dateien bewusst als Quelle der + * Wahrheit gelten sollen – vorher ein DB-Backup ziehen: + * + * $seeder = new CmsContentSeeder; + * $seeder->overwriteExisting = true; + * $seeder->run(); + */ + public bool $overwriteExisting = false; + + /** + * Nur diese Section-Keys verarbeiten (leer = alle). + * + * Erlaubt einen gezielten Lauf für einzelne Sektionen, ohne den Rest anzufassen. + * + * @var array + */ + public array $onlySections = []; + /** * Sections to skip (not migrated, handled elsewhere or irrelevant). * @@ -52,9 +75,14 @@ class CmsContentSeeder extends Seeder $created = 0; $updated = 0; + $skipped = 0; foreach ($sectionMap as $sectionKey => $groupKeyPair) { - if (in_array($sectionKey, $this->skipSections, true)) { + if ($this->onlySections !== [] && ! in_array($sectionKey, $this->onlySections, true)) { + continue; + } + + if ($this->onlySections === [] && in_array($sectionKey, $this->skipSections, true)) { continue; } @@ -83,6 +111,12 @@ class CmsContentSeeder extends Seeder ]; if ($content) { + if (! $this->overwriteExisting) { + $skipped++; + + continue; + } + $content->update([ 'type' => $type, 'value' => $value, @@ -100,7 +134,15 @@ class CmsContentSeeder extends Seeder } } - $this->command?->info("CmsContent: {$created} created, {$updated} updated."); + $this->command?->info("CmsContent: {$created} created, {$updated} updated, {$skipped} kept (existing CMS content)."); + + if ($skipped > 0 && ! $this->overwriteExisting) { + $this->command?->comment('Bestehende CMS-Inhalte wurden nicht angetastet. Zum Überschreiben $overwriteExisting = true setzen.'); + } + + if ($created > 0 || $updated > 0) { + app(CmsContentService::class)->clearCache(); + } } private function detectType(mixed $data): string diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index b40ae54c..6e701437 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -14,14 +14,21 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - User::factory()->create([ - 'name' => 'Kevin Adametz', - 'email' => 'kevin.adametz@me.com', - 'password' => Hash::make('xunfew-0Jygjy-minnyt'), + 'name' => config('seeding.admin_name'), + 'email' => config('seeding.admin_email'), + 'password' => Hash::make(config('seeding.admin_password')), ]); - $this->call(TestDisplaySeeder::class); + // CMS-Inhalte: legen nur an, was fehlt – bestehende Redaktion bleibt unangetastet. + $this->call([ + CmsContentSeeder::class, + CmsLegalSeeder::class, + NetzwerkPageCmsSeeder::class, + CmsArticleSeeder::class, + CmsBrandSeeder::class, + CmsProjectSeeder::class, + TestDisplaySeeder::class, + ]); } } diff --git a/database/seeders/DemoBrandGalleryCleanupSeeder.php b/database/seeders/DemoBrandGalleryCleanupSeeder.php new file mode 100644 index 00000000..e2d56507 --- /dev/null +++ b/database/seeders/DemoBrandGalleryCleanupSeeder.php @@ -0,0 +1,52 @@ +whereNotNull('gallery')->get() as $brand) { + $gallery = CmsBrand::normalizeGallery($brand->gallery); + $kept = array_values(array_filter( + $gallery, + fn (array $item) => ! in_array($item['image'], $demoImages, true), + )); + + if (count($kept) === count($gallery)) { + continue; + } + + $removed += count($gallery) - count($kept); + $brand->gallery = $kept; + $brand->save(); + } + + $this->command?->info("Demobilder aus den Galerien entfernt: {$removed}."); + } +} diff --git a/database/seeders/DemoBrandGallerySeeder.php b/database/seeders/DemoBrandGallerySeeder.php new file mode 100644 index 00000000..3aed63f8 --- /dev/null +++ b/database/seeders/DemoBrandGallerySeeder.php @@ -0,0 +1,70 @@ +> + */ + public const GALLERIES = [ + 'cabinet' => [ + ['b2a.webp', CmsBrand::GALLERY_PROJECT, 'Wohnwand mit Einbauregal, Bielefeld'], + ['hero-room.webp', CmsBrand::GALLERY_PROJECT, 'Wohnbereich mit Raumteiler'], + ['stileigentum.webp', CmsBrand::GALLERY_INSPIRATION, 'Entwurf Wohnzimmer mit Sideboard'], + ['best-of-two-worlds.jpg', CmsBrand::GALLERY_INSPIRATION, 'Visualisierung offener Wohnraum'], + ], + 'rotpunkt' => [ + ['best-of-two-world.jpg', CmsBrand::GALLERY_PROJECT, 'Offener Wohn- und Kochbereich'], + ['style2own.webp', CmsBrand::GALLERY_INSPIRATION, 'Entwurf heller Wohnbereich'], + ['magazin-3.webp', CmsBrand::GALLERY_INSPIRATION, 'Visualisierung Wohnraum mit Ausblick'], + ], + 'februe' => [ + ['accommodation-2.webp', CmsBrand::GALLERY_PROJECT, 'Empfangsbereich eines Buerogebaeudes'], + ['accommodation-1.webp', CmsBrand::GALLERY_PROJECT, 'Grossraumbuero mit Arbeitsplaetzen'], + ['partner-hero.webp', CmsBrand::GALLERY_INSPIRATION, 'Entwurf Besprechungsbereich'], + ['ecosystem-hero.webp', CmsBrand::GALLERY_INSPIRATION, 'Visualisierung Projektraum'], + ], + ]; + + public function run(): void + { + foreach (self::GALLERIES as $slug => $items) { + $brand = CmsBrand::query()->where('slug', $slug)->first(); + + if (! $brand) { + $this->command?->warn("Marke „{$slug}“ nicht gefunden – uebersprungen."); + + continue; + } + + $brand->gallery = CmsBrand::normalizeGallery(array_map( + fn (array $item) => ['image' => $item[0], 'category' => $item[1], 'caption' => $item[2]], + $items, + )); + $brand->save(); + + $this->command?->info("Galerie fuer „{$brand->slug}“ gesetzt: ".count($brand->gallery).' Bilder.'); + } + } +} diff --git a/database/seeders/DisplayInteriorBrandingSeeder.php b/database/seeders/DisplayInteriorBrandingSeeder.php new file mode 100644 index 00000000..00f8e257 --- /dev/null +++ b/database/seeders/DisplayInteriorBrandingSeeder.php @@ -0,0 +1,50 @@ +where('type', DisplayVersionType::B2in)->get() as $module) { + $settings = $module->settings ?? []; + + $settings['header_logo_url'] = ($settings['theme'] ?? 'dark') === 'light' + ? self::LOGO_LIGHT + : self::LOGO_DARK; + + $settings['header_claim'] = self::CLAIM; + + $module->settings = $settings; + $module->save(); + $updated++; + } + + $this->command?->info("Display-Branding vereinheitlicht: {$updated} B2in-Module."); + } +} diff --git a/database/seeders/DisplayMediaUrlNormalizerSeeder.php b/database/seeders/DisplayMediaUrlNormalizerSeeder.php new file mode 100644 index 00000000..c8df32ac --- /dev/null +++ b/database/seeders/DisplayMediaUrlNormalizerSeeder.php @@ -0,0 +1,77 @@ +normalize($module->settings ?? []); + + if ($settings !== ($module->settings ?? [])) { + $module->settings = $settings; + $module->save(); + } + } + + foreach (DisplayVersionItem::all() as $item) { + $content = $this->normalize($item->content ?? []); + + if ($content !== ($item->content ?? [])) { + $item->content = $content; + $item->save(); + } + } + + $this->command?->info("Display-Medien-URLs host-unabhängig gemacht: {$this->rewritten} Verweise."); + } + + /** + * @param array $value + * @return array + */ + private function normalize(array $value): array + { + foreach ($value as $key => $entry) { + if (is_array($entry)) { + $value[$key] = $this->normalize($entry); + + continue; + } + + if (is_string($entry) && preg_match(self::ASSET_URL_PATTERN, $entry, $matches) === 1) { + $value[$key] = $matches[1]; + $this->rewritten++; + } + } + + return $value; + } +} diff --git a/database/seeders/InteriorRelaunchCmsSeeder.php b/database/seeders/InteriorRelaunchCmsSeeder.php new file mode 100644 index 00000000..9b085aab --- /dev/null +++ b/database/seeders/InteriorRelaunchCmsSeeder.php @@ -0,0 +1,70 @@ + + */ + private const RELAUNCH_SECTIONS = [ + // Global + 'announcement_bar', + 'header', + 'footer', + 'cta_section', + + // Startseite + 'hero', + 'founder_bar', + 'audience_switch', + 'core_brands', + 'vision_section', + 'sponsoring', + 'immobilien_teaser', + + // Unterseite /marken + 'marken_hero', + 'marken_network', + 'marken_growth', + 'marken_cta', + + // Unterseiten – Durchgang auf Alt-Begriffe + 'about_hero', + 'our_story', + 'faq', + 'contact_form', + 'netzwerk_hero', + 'netzwerk_teasers', + 'netzwerk_image_break', + 'netzwerk_cabinet_partner', + ]; + + public function run(): void + { + $seeder = new CmsContentSeeder; + $seeder->setCommand($this->command); + $seeder->onlySections = self::RELAUNCH_SECTIONS; + $seeder->overwriteExisting = true; + $seeder->run(); + + $this->command?->info('Interior-Relaunch: '.count(self::RELAUNCH_SECTIONS).' Sektionen aus den Lang-Dateien übernommen.'); + } +} diff --git a/database/seeders/OriginSectionCmsSeeder.php b/database/seeders/OriginSectionCmsSeeder.php new file mode 100644 index 00000000..3a9dabde --- /dev/null +++ b/database/seeders/OriginSectionCmsSeeder.php @@ -0,0 +1,69 @@ + */ + private const SECTIONS = ['origin_section']; + + public function run(): void + { + $seeder = new CmsContentSeeder; + $seeder->setCommand($this->command); + $seeder->onlySections = self::SECTIONS; + $seeder->run(); + + $this->dropLegacyLocations(); + + $this->command?->info('Herkunfts-Sektion: angelegt, falls sie fehlte.'); + } + + private function dropLegacyLocations(): void + { + $row = CmsContent::query()->where('group', 'home')->where('key', 'origin_section')->first(); + + if (! $row) { + return; + } + + $value = $row->getTranslations('value'); + $changed = false; + + foreach ($value as $locale => $section) { + if (is_array($section) && array_key_exists('locations', $section)) { + unset($section['locations']); + $value[$locale] = $section; + $changed = true; + } + } + + if (! $changed) { + return; + } + + $row->value = $value; + $row->save(); + app(CmsContentService::class)->clearCache('home'); + + $this->command?->info('Herkunfts-Sektion: veralteten Schlüssel „locations“ entfernt – die Standorte kommen aus den Marken.'); + } +} diff --git a/database/seeders/OtterpohlBrandSeeder.php b/database/seeders/OtterpohlBrandSeeder.php new file mode 100644 index 00000000..34d6c791 --- /dev/null +++ b/database/seeders/OtterpohlBrandSeeder.php @@ -0,0 +1,136 @@ +where('slug', self::SLUG)->first(); + + if (! $brand) { + $this->command?->warn('Marke „'.self::SLUG.'“ nicht gefunden – erst CmsBrandSeeder ausführen.'); + + return; + } + + $brand->fill(self::data()); + + if (trim((string) $brand->logo) === '') { + $brand->logo = self::LOGO; + } + + $brand->save(); + + $this->command?->info('Otterpohl: Marke „'.self::SLUG.'“ mit den Texten vom 24.09.2026 (v2) aktualisiert.'); + } + + /** + * Alle Textfelder der Marke. Ohne slug, logo, image, gallery, order. + * + * Reihenfolge wie in der Zulieferung v2: Einleitung „Wollmilchsau“, zwei Abschnitte, die acht + * Leistungen, zum Schluss „Das Ganze statt nur das Einzelne“ – der steht als Abschlussblock + * (cta_title/cta_text), damit er auf der Seite hinter Leistungen und Galerie bleibt. + * + * @return array + */ + public static function data(): array + { + return [ + 'name' => ['de' => 'Otterpohl Möbelservice', 'en' => 'Otterpohl Möbelservice'], + 'tagline' => ['de' => 'Nicht easy click & buy. Aber dennoch easy to get.', 'en' => 'Not easy click & buy. But still easy to get.'], + 'teaser' => [ + 'de' => 'Individuelle Möbel, Türen, Treppen, Küchen, Wandverkleidungen und Objektbau – bis hin zum kompletten Innenausbau. Tischlerhandwerk aus Rheda-Wiedenbrück.', + 'en' => 'Bespoke furniture, doors, stairs, kitchens, wall cladding and contract work – up to complete interior fit-outs. Joinery from Rheda-Wiedenbrück.', + ], + 'summary' => [ + 'de' => 'Individuelle Möbel. Türen. Treppen. Küchen. Wandverkleidungen. Objektbau. Sonderanfertigungen. Lackierungen und Echtholzfurniere. Bis hin zum kompletten Innenausbau. Mit Otterpohl Möbelservice haben wir einen Partner an unserer Seite, bei dem die Möglichkeiten nicht am einzelnen Möbelstück aufhören. + +Denn manchmal braucht ein Raum mehr als einen Schrank oder ein Sideboard. Vielleicht soll eine Treppe Teil des Gestaltungskonzepts werden. Eine Wand erhält eine individuelle Holzverkleidung. Oder Möbel, Materialien und bauliche Elemente müssen so zusammenspielen, dass am Ende alles wie aus einem Guss wirkt. Genau dort beginnt der individuelle Innenausbau.', + 'en' => 'Bespoke furniture. Doors. Stairs. Kitchens. Wall cladding. Contract work. Special constructions. Lacquer and real-wood veneer. Up to complete interior fit-outs. With Otterpohl Möbelservice we have a partner whose possibilities do not end at the single piece of furniture. + +Sometimes a room needs more than a wardrobe or a sideboard. A staircase becomes part of the design. A wall gets individual wood cladding. Or furniture, materials and built elements have to work together so that everything looks as if cast from one mould. That is where individual interior fit-out begins.', + ], + 'highlights' => [ + 'Individuelle Möbel, Türen, Treppen und Küchen', + 'Innenausbau, Objektbau, Lack und Echtholzfurnier', + 'Planung und Handwerk aus einer Hand – inklusive Montage', + ], + 'description' => ['de' => '', 'en' => ''], + 'services' => [ + ['title' => 'Individuelle Möbel & Sonderanfertigungen', 'description' => 'Möbel nach Maß, besondere Konstruktionen und individuelle Lösungen – nach Planung, Idee, Skizze oder konkreter Anforderung.'], + ['title' => 'Innenausbau', 'description' => 'Von einzelnen Ausbauarbeiten bis zum ganzheitlichen Raumkonzept – inklusive individueller Verkleidungen und maßgefertigter Einbauten.'], + ['title' => 'Türen & Treppen', 'description' => 'Individuelle Türen, Treppen und konstruktive Lösungen, die sich in Architektur und Einrichtung einfügen.'], + ['title' => 'Küchen', 'description' => 'Individuelle Küchenlösungen und handwerkliche Ergänzungen auch dort, wo klassische Küchenprogramme an ihre Grenzen kommen.'], + ['title' => 'Wandverkleidungen & besondere Details', 'description' => 'Holzverkleidungen, Lamellen, Paneele und individuelle Gestaltungselemente, die Architektur und Einrichtung miteinander verbinden.'], + ['title' => 'Objektbau', 'description' => 'Individuelle Lösungen für Büros, Verkaufsräume und gewerblich genutzte Flächen – vom Einzelmöbel bis zum kompletten Ausbau.'], + ['title' => 'Lack & Echtholzfurnier', 'description' => 'Individuelle Lackierungen und Furniere für Lösungen, bei denen Farbe, Material und Oberfläche exakt aufeinander abgestimmt werden sollen.'], + ['title' => 'Montage', 'description' => 'Die Umsetzung endet nicht bei der Fertigung. Entscheidend ist, dass Möbel, Ausbau und Raum am Ende auch vor Ort sauber zusammenkommen.'], + ], + 'sections' => [ + [ + 'title' => 'Ziemlich nah an der eierlegenden Wollmilchsau', + 'paragraphs' => [ + 'Der Begriff ist vielleicht nicht besonders elegant. Aber ganz falsch ist er nicht.', + 'Individuelle Möbel. Türen. Treppen. Küchen. Wandverkleidungen. Objektbau. Sonderanfertigungen. Lackierungen und Echtholzfurniere. Bis hin zum kompletten Innenausbau.', + 'Mit Otterpohl Möbelservice haben wir einen Partner an unserer Seite, bei dem die Möglichkeiten nicht am einzelnen Möbelstück aufhören.', + 'Denn manchmal braucht ein Raum mehr als einen Schrank oder ein Sideboard. Vielleicht soll eine Treppe Teil des Gestaltungskonzepts werden. Eine Wand erhält eine individuelle Holzverkleidung. Eine Tür soll sich selbstverständlich in den Raum einfügen. Oder Möbel, Materialien und bauliche Elemente müssen so zusammenspielen, dass am Ende alles wie aus einem Guss wirkt.', + ], + 'claim' => 'Genau dort beginnt der individuelle Innenausbau.', + ], + [ + 'title' => 'Vom einzelnen Möbel bis zum ganzen Raum', + 'paragraphs' => [ + 'Wir arbeiten mit Herstellern, die schon sehr viele individuelle Möglichkeiten bieten. CABINET, Rotpunkt und Febrü geben uns dafür eine starke Basis. Aber es gibt Projekte, bei denen die Lösung darüber hinausgeht.', + 'Mit Otterpohl Möbelservice können wir deshalb nicht nur individuelle Möbel und Sonderanfertigungen realisieren, sondern auch Türen, Treppen, Küchen, Wand- und Deckenverkleidungen sowie komplette Innenausbauten und Lösungen für den Objektbereich einbeziehen.', + ], + 'claim' => 'So können wir Räume ganzheitlicher denken – und Details nicht isoliert betrachten.', + ], + [ + 'title' => 'Planung trifft Handwerk', + 'paragraphs' => [ + 'Manchmal entsteht die beste Lösung aus einem Möbelprogramm. Manchmal braucht es eine individuelle Ergänzung. Und manchmal muss etwas komplett neu gedacht und gebaut werden. Genau diese Kombination macht für uns den Unterschied.', + 'Wir planen den Raum und verbinden bei Bedarf unterschiedliche Hersteller, Materialien und handwerkliche Lösungen miteinander. Otterpohl Möbelservice ergänzt diese Planung um die Möglichkeiten einer klassischen Tischlerei und eines erfahrenen Innenausbauers.', + ], + 'claim' => 'Das Ergebnis kann ein einzelnes Möbel sein. Oder ein kompletter Raum.', + ], + ], + 'widget_label' => ['de' => 'Persönliche Beratung zur Maßarbeit vereinbaren', 'en' => 'Arrange a personal consultation on bespoke work'], + 'cta_title' => ['de' => 'Das Ganze statt nur das Einzelne', 'en' => 'The whole, not just the single piece'], + 'cta_text' => [ + 'de' => 'Genau deshalb passt Otterpohl Möbelservice zu unserem Ansatz: Wir möchten Räume nicht in einzelne Gewerke zerlegen, sondern Lösungen schaffen, bei denen Möbel, Materialien, Architektur und handwerkliche Details zusammenpassen. Mal braucht es dafür einen Hersteller. Mal einen Tischler. Und manchmal beides. Nicht easy click & buy. Aber dennoch easy to get.', + 'en' => 'That is why Otterpohl Möbelservice fits our approach: we do not want to split rooms into single trades, but create solutions where furniture, materials, architecture and craft details belong together. Sometimes that takes a manufacturer. Sometimes a joiner. And sometimes both. Not easy click & buy. But still easy to get.', + ], + 'origin_city' => ['de' => 'Rheda-Wiedenbrück', 'en' => 'Rheda-Wiedenbrück'], + 'origin_distance' => ['de' => 'rund 35 km', 'en' => 'about 35 km'], + 'origin_role' => [ + 'de' => 'Tischlerei und Innenausbau aus Rheda-Wiedenbrück – vom einzelnen Möbel bis zum ganzen Raum, inklusive Montage.', + 'en' => 'Joinery and interior fit-out from Rheda-Wiedenbrück – from a single piece to the whole room, installation included.', + ], + 'showroom' => '', + 'is_published' => true, + ]; + } +} diff --git a/database/seeders/WhatsappNumberSeeder.php b/database/seeders/WhatsappNumberSeeder.php new file mode 100644 index 00000000..4426c432 --- /dev/null +++ b/database/seeders/WhatsappNumberSeeder.php @@ -0,0 +1,83 @@ + + */ + private const PLACEHOLDERS = ['+49 151 00000000', '+49 521 98610020']; + + /** @var array */ + private const LOCALES = ['de', 'en']; + + public function run(): void + { + $row = CmsContent::query()->where('group', 'global')->where('key', 'contact')->first(); + + if (! $row) { + CmsContent::create([ + 'group' => 'global', + 'key' => 'contact', + 'type' => 'json', + 'value' => array_fill_keys(self::LOCALES, ['whatsapp' => self::NUMBER]), + 'order' => 0, + ]); + app(CmsContentService::class)->clearCache('global'); + + $this->command?->info('WhatsApp-Nummer: Eintrag global.contact angelegt.'); + + return; + } + + $value = $row->getTranslations('value'); + $changed = 0; + + foreach (self::LOCALES as $locale) { + $section = is_array($value[$locale] ?? null) ? $value[$locale] : []; + $current = trim((string) ($section['whatsapp'] ?? '')); + + if ($current !== '' && ! in_array($current, self::PLACEHOLDERS, true)) { + continue; + } + + $section['whatsapp'] = self::NUMBER; + $value[$locale] = $section; + $changed++; + } + + if ($changed === 0) { + $this->command?->info('WhatsApp-Nummer: bereits gepflegt, nichts geändert.'); + + return; + } + + $row->value = $value; + $row->save(); + app(CmsContentService::class)->clearCache('global'); + + $this->command?->info("WhatsApp-Nummer gesetzt ({$changed} Sprache(n))."); + } +} diff --git a/database/seeders/WorldSplitCmsSeeder.php b/database/seeders/WorldSplitCmsSeeder.php new file mode 100644 index 00000000..50eec7e1 --- /dev/null +++ b/database/seeders/WorldSplitCmsSeeder.php @@ -0,0 +1,63 @@ + */ + private const NEW_SECTIONS = [ + 'header_immobilien', + 'announcement_bar_immobilien', + 'magazin_list_immobilien', + 'faq_immobilien', + 'contact_form_immobilien', + 'cta_section_immobilien', + 'immobilien_about_hero', + 'immobilien_our_story', + 'immobilien_about_image_break', + 'immobilien_our_values', + ]; + + /** @var array */ + private const OVERWRITE_SECTIONS = [ + 'header', + 'magazin_list', + // Betreffe in Gruppen (Interior / Immobilien / B2in) statt flacher Liste, 15.09. + 'contact_form', + 'contact_form_immobilien', + ]; + + public function run(): void + { + $create = new CmsContentSeeder; + $create->setCommand($this->command); + $create->onlySections = self::NEW_SECTIONS; + $create->run(); + + $overwrite = new CmsContentSeeder; + $overwrite->setCommand($this->command); + $overwrite->onlySections = self::OVERWRITE_SECTIONS; + $overwrite->overwriteExisting = true; + $overwrite->run(); + + $this->command?->info('Welten-Trennung: '.count(self::NEW_SECTIONS).' Sektionen angelegt (falls fehlend), '.count(self::OVERWRITE_SECTIONS).' Interior-Sektionen aktualisiert.'); + } +} diff --git a/dev/12-01-2026/Moebeldatenliste Stand 4.11.2025.csv b/dev/2026-01-12/Moebeldatenliste Stand 4.11.2025.csv similarity index 100% rename from dev/12-01-2026/Moebeldatenliste Stand 4.11.2025.csv rename to dev/2026-01-12/Moebeldatenliste Stand 4.11.2025.csv diff --git a/dev/12-01-2026/db-backup.sql b/dev/2026-01-12/db-backup.sql similarity index 100% rename from dev/12-01-2026/db-backup.sql rename to dev/2026-01-12/db-backup.sql diff --git a/dev/12-01-2026/entwicklungsplan.md b/dev/2026-01-12/entwicklungsplan.md similarity index 100% rename from dev/12-01-2026/entwicklungsplan.md rename to dev/2026-01-12/entwicklungsplan.md diff --git a/dev/12-01-2026/konzeption.md b/dev/2026-01-12/konzeption.md similarity index 100% rename from dev/12-01-2026/konzeption.md rename to dev/2026-01-12/konzeption.md diff --git a/dev/27-02-2026/B2in Website-Architektur.md b/dev/2026-02-27/B2in Website-Architektur.md similarity index 100% rename from dev/27-02-2026/B2in Website-Architektur.md rename to dev/2026-02-27/B2in Website-Architektur.md diff --git a/dev/27-02-2026/b2in-local-for-local.md b/dev/2026-02-27/b2in-local-for-local.md similarity index 100% rename from dev/27-02-2026/b2in-local-for-local.md rename to dev/2026-02-27/b2in-local-for-local.md diff --git a/dev/27-02-2026/b2in-umsetzung-changelog.md b/dev/2026-02-27/b2in-umsetzung-changelog.md similarity index 100% rename from dev/27-02-2026/b2in-umsetzung-changelog.md rename to dev/2026-02-27/b2in-umsetzung-changelog.md diff --git a/dev/27-02-2026/b2in-website-umsetzungsplan.md b/dev/2026-02-27/b2in-website-umsetzungsplan.md similarity index 100% rename from dev/27-02-2026/b2in-website-umsetzungsplan.md rename to dev/2026-02-27/b2in-website-umsetzungsplan.md diff --git a/dev/27-02-2026/backup-before-relaunch/about.blade.php b/dev/2026-02-27/backup-before-relaunch/about.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/about.blade.php rename to dev/2026-02-27/backup-before-relaunch/about.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/b2in.blade.php b/dev/2026-02-27/backup-before-relaunch/b2in.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/b2in.blade.php rename to dev/2026-02-27/backup-before-relaunch/b2in.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/contact.blade.php b/dev/2026-02-27/backup-before-relaunch/contact.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/contact.blade.php rename to dev/2026-02-27/backup-before-relaunch/contact.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/content.php b/dev/2026-02-27/backup-before-relaunch/content.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/content.php rename to dev/2026-02-27/backup-before-relaunch/content.php diff --git a/dev/27-02-2026/backup-before-relaunch/ecosystem.blade.php b/dev/2026-02-27/backup-before-relaunch/ecosystem.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/ecosystem.blade.php rename to dev/2026-02-27/backup-before-relaunch/ecosystem.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/footer.blade.php b/dev/2026-02-27/backup-before-relaunch/footer.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/footer.blade.php rename to dev/2026-02-27/backup-before-relaunch/footer.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/hero.blade.php b/dev/2026-02-27/backup-before-relaunch/hero.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/hero.blade.php rename to dev/2026-02-27/backup-before-relaunch/hero.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/home.blade.php b/dev/2026-02-27/backup-before-relaunch/home.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/home.blade.php rename to dev/2026-02-27/backup-before-relaunch/home.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/magazin.blade.php b/dev/2026-02-27/backup-before-relaunch/magazin.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/magazin.blade.php rename to dev/2026-02-27/backup-before-relaunch/magazin.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/partner.blade.php b/dev/2026-02-27/backup-before-relaunch/partner.blade.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/partner.blade.php rename to dev/2026-02-27/backup-before-relaunch/partner.blade.php diff --git a/dev/27-02-2026/backup-before-relaunch/web.php b/dev/2026-02-27/backup-before-relaunch/web.php similarity index 100% rename from dev/27-02-2026/backup-before-relaunch/web.php rename to dev/2026-02-27/backup-before-relaunch/web.php diff --git a/dev/27-02-2026/review-architektur-02-03-2026.md b/dev/2026-02-27/review-architektur-02-03-2026.md similarity index 100% rename from dev/27-02-2026/review-architektur-02-03-2026.md rename to dev/2026-02-27/review-architektur-02-03-2026.md diff --git a/dev/12-03-2026/content.md b/dev/2026-03-12/content.md similarity index 100% rename from dev/12-03-2026/content.md rename to dev/2026-03-12/content.md diff --git a/dev/12-03-2026/magazin.md b/dev/2026-03-12/magazin.md similarity index 100% rename from dev/12-03-2026/magazin.md rename to dev/2026-03-12/magazin.md diff --git a/dev/12-03-2026/tasks.md b/dev/2026-03-12/tasks.md similarity index 100% rename from dev/12-03-2026/tasks.md rename to dev/2026-03-12/tasks.md diff --git a/dev/2026-08-28/B2in_Interior-Konzept.md b/dev/2026-08-28/B2in_Interior-Konzept.md new file mode 100644 index 00000000..6620f08d --- /dev/null +++ b/dev/2026-08-28/B2in_Interior-Konzept.md @@ -0,0 +1,102 @@ +# B2in Interieur +## Konzept zur Neuausrichtung der Website – Richtung, Struktur und Textauszüge + +*Adametz.Media | Kevin Adametz | Juli 2026 | Zur Abstimmung* + +--- + +Hallo Marcel, dieses Dokument fasst zusammen, wie ich die Website auf die neue Ausrichtung umbaue – Struktur, Gewichtung und ausgewählte Textauszüge, damit Du die Richtung beurteilen kannst. Es sind bewusst nicht alle Texte enthalten, sondern die Stellen, die den Ton setzen. Nimm Dir in Ruhe die Zeit, es durchzugehen; Dein gesammeltes Feedback baue ich nach meinem Urlaub direkt ein. + +--- + +## 1. Die neue Ausrichtung in drei Sätzen + +B2in stellt künftig das Interieur und die Kernmarken nach vorne – CABINET, Rotpunkt Küchen, Febrü und weitere. Die Immobilien bleiben Teil des Angebots, rücken aber bewusst nach hinten: Ergänzung statt Aufmacher. Deine Idee der Doppelbedeutung nehme ich dabei auf: + +> **B2in – Bridges to International. Bridges to Interior.** +> +> *Die internationale Herkunft bleibt Teil der Geschichte, das Interieur wird der Kern. Im Logo wird das sichtbar: B2in mit dem Zusatz „Interieur".* + +Wichtiges Prinzip dabei: **B2in Interieur ist das Dach – die Kernmarken sind die Gesichter.** Jede Marke behält ihren eigenen Auftritt und ihre eigene Stärke; B2in verbindet sie, ohne sie zu vermischen. Genau das unterscheidet eine Premium-Gruppe von einem Gemischtwarenladen. + +--- + +## 2. Der neue Seitenaufbau + +Die Seite wird nicht neu gebaut – Struktur und Technik bleiben. Es ändern sich Reihenfolge, Gewichtung und Inhalte. So sieht der Aufbau der Startseite künftig aus: + +| # | Bereich | Was passiert | +|---|---------|--------------| +| 1 | **Ankündigungsleiste** | Bleibt als Instrument, trägt künftig Interieur-Themen (z.B. Neuheiten aus den Showrooms) statt dauerhaft Immobilien-Launches. | +| 2 | **Hero** | Neue Headline und neue Bildwelt: Interieur und Marken zuerst. Zwei Buttons: „Unsere Marken entdecken" und „Termin anfragen". | +| 3 | **Weiche Privat / Geschäft** | Neu: Zwei klare Wege direkt unter dem Einstieg – Privatkunden (Einrichtung fürs Zuhause) und Geschäftskunden (Ausstattung für Projekte, „bis zur letzten Blume"). | +| 4 | **Kernmarken** | Neu: Das Herzstück. Kacheln für CABINET, Rotpunkt, Febrü – über das CMS von Dir selbst erweiterbar, jede Marke mit eigener Detailseite. | +| 5 | **Über Dich / Vertrauen** | Bleibt – Du als Gesicht und persönlicher Ansprechpartner. Text dreht sich: Einrichtungskompetenz zuerst, Immobilien als ergänzende Expertise am Ende. | +| 6 | **Aktuelles (Display)** | Neu: Die Inhalte aus dem Display-CMS erscheinen auf der Website (9:16). Dazu kommt Deine neue, selbst pflegbare Rubrik-Seite als eigenes Web-Display. | +| 7 | **Immobilien-Teaser** | Bleibt, aber klein und dezent am Ende der Seite – wer gezielt sucht, findet den Weg; wer wegen Einrichtung kommt, wird nicht abgelenkt. | +| 8 | **Abschluss & Kontakt** | Terminanfrage-Widget (Name, E-Mail, Wunschtermin, Notiz, Markenauswahl) plus WhatsApp-Button, der direkt den Chat mit Dir öffnet. | + +In der Navigation rückt **„Marken" nach vorne, „Immobilien" nach hinten**. Auch Titel und Beschreibung für Google werden auf die neue Ausrichtung umgestellt, damit die Seite auch in der Suche die richtige Geschichte erzählt. + +--- + +## 3. Textauszüge – so klingt die neue Richtung + +Drei Auszüge, die den Ton setzen. Alle weiteren Texte folgen dieser Linie. + +### 3.1 Hero (Einstieg) + +> **B2in Interieur – Premium-Einrichtung von starken Marken. Für Ihr Zuhause. Für Ihr Projekt.** +> +> *Von der einzelnen Maßanfertigung bis zur vollständigen Ausstattung – mit CABINET, Rotpunkt Küchen, Febrü und einem Netzwerk regionaler Fachexperten.* + +### 3.2 Die Weiche: Privat / Geschäft + +> **Einrichtung für Ihr Zuhause** +> +> Einzelne Möbelstücke, maßgefertigte Einbaulösungen oder die komplette Einrichtung: Unsere Kernmarken und unser Fachhandels-Netzwerk begleiten Sie von der ersten Idee bis zur Montage. + +> **Ausstattung für Ihr Projekt** +> +> Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk. + +### 3.3 Beispiel Kernmarken-Kachel (CABINET) + +> **CABINET – Einbauschränke nach Maß.** +> +> Maßgefertigte Schranklösungen, geplant bis auf den Millimeter – gefertigt für Räume, in denen Standard nicht genügt. Erleben Sie die Möglichkeiten in unserem Showroom in Bielefeld. + +Rotpunkt und Febrü bekommen Kacheln im selben Stil. Bei Febrü setze ich den Schwerpunkt auf das Zuhause-Arbeiten (Home-Office) und nenne die Büro- und Unternehmensausstattung als zweiten Aspekt – so passt die Marke in beide Welten, mit der richtigen Gewichtung. + +--- + +## 4. Was bewusst so bleibt + +- **Die Immobilien** bleiben statisch wie bisher und rücken nur in der Gewichtung nach hinten. Der Bereich selbst bleibt vollständig erreichbar. +- **Das Magazin** bleibt bestehen – die neue Rubrik-Seite ergänzt es, ersetzt es aber nicht. +- **Videos** laufen über YouTube-Einbindung – unkompliziert und ohne eigene Videotechnik. Das Influencer-Video bekommt darüber einen gut sichtbaren Platz. + +--- + +## 5. Dein Feedback – diese Punkte entscheide bitte Du + +Damit ich nach dem Urlaub direkt umsetzen kann, brauche ich zu diesen Punkten Deine Rückmeldung: + +| Punkt | Frage an Dich | +|-------|---------------| +| **Logo-Zusatz „Interieur"** | Freigabe der Ergänzung im Logo. Kläre bitte auch, ob Du eine Logo-Datei anpassen lässt oder ich das übernehmen soll. | +| **Claim** | Vorschlag: „Premium-Einrichtung. Persönlich gemacht." (ersetzt „Connecting Design and Property"). Passt das – oder soll „Bridges to Interior" selbst als Claim-Zeile erscheinen? | +| **Hero-Headline** | Passt Dir die Richtung aus Auszug 3.1? | +| **Febrü-Tonalität** | Home-Office zuerst, Büro als zweiter Aspekt – einverstanden? | +| **Name der neuen Rubrik** | „Aktuelles", „Referenzen", „Projekte" – oder ein anderer Name? | +| **Bildmaterial** | Für Hero und Kernmarken brauche ich hochwertige Motive (Logos und Basis-Infos zu Rotpunkt und Febrü ebenfalls). Was kannst Du liefern, was soll ich auswählen? | + +--- + +## 6. Zeitplan + +- **Bis Anfang August:** Du gehst dieses Konzept in Ruhe durch und sammelst Dein Feedback zu den Punkten aus Abschnitt 5. +- **Ab meiner Rückkehr:** Ich baue Dein Feedback ein und setze Struktur, Texte und Funktionen um. Zwischenstände zeige ich Dir vor dem Livegang. +- **Ende August:** Livegang der neuen Ausrichtung – vorausgesetzt, Feedback und Zulieferungen (Bilder, Logos, Marken-Infos) liegen rechtzeitig vor. + +*Bei Fragen erreichst Du mich nach dem Urlaub wie gewohnt – dringende Punkte gerne per Mail, ich arbeite sie der Reihe nach ab.* \ No newline at end of file diff --git a/dev/2026-08-28/Bildschirmfoto 2026-09-24 um 17.30.16.png b/dev/2026-08-28/Bildschirmfoto 2026-09-24 um 17.30.16.png new file mode 100644 index 00000000..95ac6279 Binary files /dev/null and b/dev/2026-08-28/Bildschirmfoto 2026-09-24 um 17.30.16.png differ diff --git a/dev/2026-08-28/Farbwelt und Stil.png b/dev/2026-08-28/Farbwelt und Stil.png new file mode 100644 index 00000000..f6757b2a Binary files /dev/null and b/dev/2026-08-28/Farbwelt und Stil.png differ diff --git a/dev/2026-08-28/Februe_Webseitentext_B2in_FINAL.pdf b/dev/2026-08-28/Februe_Webseitentext_B2in_FINAL.pdf new file mode 100644 index 00000000..eb1ba303 Binary files /dev/null and b/dev/2026-08-28/Februe_Webseitentext_B2in_FINAL.pdf differ diff --git a/dev/2026-08-28/Februe_Webseitentext_FINAL.md b/dev/2026-08-28/Februe_Webseitentext_FINAL.md new file mode 100644 index 00000000..b1a53a1a --- /dev/null +++ b/dev/2026-08-28/Februe_Webseitentext_FINAL.md @@ -0,0 +1,236 @@ +# B2in Interior · Febrü + +**Arbeiten, wo man gerne bleibt.** + +Webseitentext und Umsetzungsstruktur für die Febrü-Seite +Stand: 31. August 2026 · Korrigierte Fassung zur Umsetzung + +--- + +## Umsetzungshinweise (nicht veröffentlichen) + +**Webstruktur:** Der normale Seitentext bleibt direkt sichtbar. Alle als `AUFKLAPPBARER DETAILBEREICH` gekennzeichneten Vertiefungen werden als Akkordeon umgesetzt. So bleibt die Seite schnell erfassbar, ohne dass Inhalt verloren geht. + +**CTA:** Kein zweiter Button. Das bereits vorhandene schwebende Terminanfrage-Widget erhält auf dieser Markenseite die Beschriftung **„Persönliche Arbeitsplatzberatung vereinbaren"**. Es bleibt beim Scrollen sichtbar und darf auf dem Smartphone keine Inhalte verdecken. + +--- + +## Hero + +Ein Büro ist kein Parkplatz für Schreibtische. Es ist ein Raum für Konzentration, Austausch, Bewegung und neue Ideen. Und manchmal entsteht die beste Idee eben nicht am Schreibtisch, sondern zwischen Kaffeemaschine, Lounge und einem guten Gespräch. + +Febrü verbindet ergonomische Arbeitsplätze mit wohnlichem Design, intelligentem Stauraum, durchdachter Akustik und flexiblen Raumstrukturen. + +Wir entwickeln daraus Arbeitswelten, die zu den Menschen, ihren Aufgaben und ihrer Umgebung passen – vom einzelnen Homeoffice bis zur vollständigen Unternehmensfläche. + +**Ihr Arbeitsplatz. Persönlich geplant.** + +--- + +## Zuhause oder Projekt. Zwei Wege. Derselbe Anspruch. + +Unsere Startseite führt bewusst über zwei Wege: Zuhause und Projekt. Denn ein einzelnes Homeoffice stellt andere Anforderungen als eine Büroetage mit fünfzig Arbeitsplätzen. + +Unterschiedliche Aufgaben brauchen unterschiedliche Planung und natürlich auch unterschiedliche Kalkulationen. Bei uns gibt es trotzdem nur Kunden – mit unterschiedlichen Räumen, Anforderungen und Stückzahlen. Der Anspruch an Produkt, Beratung und Service bleibt derselbe. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Bei uns gibt es Kunden – keine Schubladen +> +> B2B und B2C sind Begriffe aus Präsentationen und Vertriebsschulungen. Früher hieß es Privatkunde oder Firmenkunde. Oder, noch schlimmer: A-, B- oder C-Kunde. Bei diesem Schubladendenken machen wir nicht mit. +> +> Natürlich beeinflusst die Menge den Preis. Wer fünfzig Arbeitsplätze einrichtet, erhält eine andere Kalkulation als jemand, der einen einzelnen Schreibtisch für sein Homeoffice sucht. Alles andere wäre weder ehrlich noch kaufmännisch sinnvoll. +> +> Die Menge verändert aber weder die Qualität des Produktes noch die Sorgfalt der Planung, die persönliche Beratung oder den Service. Ein einzelner Arbeitsplatz im privaten Zuhause verdient dieselbe Aufmerksamkeit wie eine vollständige Büroetage. +> +> Febrü bleibt Febrü. B2in bleibt B2in. Und jeder Kunde erhält eine Lösung, die zu seinem tatsächlichen Bedarf passt. + +**Ob im Kleinen oder im Großen – die Detailtreue entscheidet.** + +--- + +## Aus Herford. Für moderne Arbeitswelten. + +Febrü entwickelt und produziert hochwertige Büromöbel direkt im ostwestfälischen Herford – nur wenige Kilometer von Bielefeld entfernt. Eine Fertigungstiefe von 95 Prozent ermöglicht kurze Wege, verlässliche Qualität und hohe Flexibilität bei Materialien, Farben und individuellen Anforderungen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Von einem Schreibtisch bis zum vollständigen Bürotrakt +> +> Das Sortiment reicht von ergonomischen und höhenverstellbaren Schreibtischen über Konferenz- und Besprechungstische, Bürostühle und Loungeelemente bis zu Empfangslösungen, Stauraum, Akustikelementen, mobilen Medienmöbeln, Schließfachsystemen und flexiblen Raumstrukturen. +> +> Damit lässt sich ein einzelner Arbeitsplatz genauso überzeugend planen wie eine Praxis, eine Kanzlei, ein Konferenzbereich oder ein vollständiger Bürotrakt. + +**Made in Herford. Geplant in Bielefeld.** + +--- + +## Nicht drei Marken. Ein durchgängiger Raum. + +Ein Bürotrakt endet nicht am Schreibtisch. Menschen brauchen Orte zum Arbeiten, für Besprechungen und Rückzug – aber ebenso für Pausen, den gemeinsamen Kaffee, das Mittagessen und den informellen Austausch. + +Genau hier greifen Febrü, Rotpunkt und CABINET bei uns ineinander: Febrü gestaltet die Arbeitswelt, Rotpunkt die Küche im Bürotrakt und CABINET die millimetergenaue Sonderlösung. + +Entscheidend ist nicht, welcher Hersteller welches Möbel liefert. Entscheidend ist, dass am Ende alles zusammengehört. Wir stimmen Arbeitsplätze, Küche, Stauraum, Farben, Materialien, Beleuchtung und Akustik zu einem durchgängigen Konzept aufeinander ab. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Wie die drei Marken zusammenspielen +> +> Febrü gestaltet ergonomische Arbeitsplätze, Konferenzbereiche, Loungezonen, Akustik, Stauraum und flexible Raumstrukturen. +> +> Rotpunkt gestaltet Mitarbeiterküchen, Coffee-Points und gemeinsam genutzte Aufenthaltsküchen als selbstverständlichen Bestandteil der Arbeitswelt – und nicht als übrig gebliebene Funktionszeile. Gerade hier entstehen oft Gespräche, Verbindungen und Ideen, für die im offiziellen Meeting kein Platz war. +> +> CABINET übernimmt dort, wo Nischen vollständig genutzt, Schränke von Wand zu Wand und vom Boden bis zur Decke geplant oder besondere Übergänge zwischen Arbeitsplatz, Stauraum und Architektur geschaffen werden sollen. So entstehen beispielsweise maßgefertigte Garderoben, Aktenschränke, Medienlösungen und Stauraumelemente. + +**Febrü für die Arbeitswelt. Rotpunkt für die Küche. CABINET für die millimetergenaue Sonderlösung. B2in für das Ganze.** + +--- + +## Ein Arbeitsplatz. Fünfzig Arbeitsplätze. Ein Anspruch. + +Ob wir über einen einzelnen Arbeitsplatz oder einen vollständigen Bürotrakt sprechen: Das grundsätzliche Problem bleibt dasselbe. Fühlt sich der Mensch, der dort täglich arbeitet, gesehen und wertgeschätzt – oder wurde ihm lediglich irgendwo ein Tisch hingestellt? + +Die Stückzahl verändert den Umfang der Aufgabe, aber nicht ihre Bedeutung. Wir bewerten einen Arbeitsplatz deshalb nicht nach Quadratmetern oder Auftragswert, sondern danach, was er für den Menschen leisten muss. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Wertschätzung ist keine Frage der Stückzahl +> +> Gerade in Zeiten des Fachkräftemangels können Unternehmen es sich nicht leisten, Arbeitsbedingungen als Nebensache zu behandeln. Wer Top-Leistung erwartet, muss die passenden Voraussetzungen schaffen. Ein Obstkorb und kostenloses Wasser sind freundlich – ersetzen aber keinen guten Arbeitsplatz. +> +> Besonders deutlich wird das bei teilzeitbeschäftigten Müttern – selbstverständlich genauso bei Vätern und allen anderen Menschen mit familiärer Sorgeverantwortung. Teilzeit bedeutet nicht halbe Leistung. Oft wird in einem kürzeren Zeitfenster hoch konzentriert gearbeitet, während neben dem nächsten Onlinemeeting gleichzeitig Schule, Termine, Betreuung und damit unsere wichtigste gemeinsame Ressource organisiert werden: unser Nachwuchs. +> +> Gerade hier darf der Arbeitsplatz kein Provisorium aus Küchentisch, Beistelltisch oder Schlafzimmerecke sein. Er muss sofort funktionieren, Bewegung ermöglichen, technisch durchdacht sein und sich trotzdem vernünftig in das Zuhause einfügen. Für den Menschen, der ihn täglich nutzt, ist ein einzelner Arbeitsplatz keine kleine Lösung. Er ist seine vollständige Arbeitswelt. + +--- + +## Ergonomie, die im Alltag funktioniert + +Ein Arbeitsplatz muss sich dem Menschen anpassen – nicht umgekehrt. Höhenverstellbare Schreibtische und ergonomische Bürostühle schaffen Bewegung und unterstützen eine gesunde Haltung. + +Ergonomie endet aber nicht bei Tisch und Stuhl. Auch Laufwege, Greifräume, Bildschirmposition, Beleuchtung, Stauraum und der Wechsel zwischen Tätigkeiten bestimmen, wie angenehm und produktiv ein Arbeitstag verläuft. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Und nein: Wir meinen nicht nur den Weg zur Kaffeemaschine +> +> Sinnvolle Laufwege sind Teil einer guten Planung – auch wenn Kaffeemaschine und Kühlschrank zweifellos eine gewisse Bedeutung für den Arbeitsalltag haben. +> +> Das gilt für vier Stunden ebenso wie für zehn Stunden am Schreibtisch, im Unternehmen genauso wie im privaten Zuhause. Und natürlich müssen die Möbel gestalterisch zum Raum passen. Wer denkt: „Das sieht doch keiner – außer den Mitarbeitern", hat den ersten entscheidenden Fehler bereits gemacht. Die Mitarbeiter sehen es. Jeden Tag. Und sie arbeiten darin. + +Marcel Scheibe, Geschäftsführer von B2in, bringt es persönlich auf den Punkt: + +> „Ich bin, sagen wir, alt genug, um beim Aufstehen gelegentlich zu stöhnen. Und ehrlich gesagt fühle ich mich an manchen Tagen noch etwas älter. Mein Arzt sagt, ich solle weniger sitzen. Mein Beruf findet nun einmal zu einem großen Teil am Computer statt. Ergo brauche ich eine Lösung, die mir mehr Bewegung ermöglicht, ohne dass ich dafür meinen Beruf wechseln muss." + +--- + +## Office meets Interior + +Identische Schreibtischreihen werden einem modernen Arbeitstag nicht mehr gerecht. Menschen benötigen Bereiche für Konzentration, spontane Gespräche, Teamarbeit, Videokonferenzen, vertrauliche Besprechungen und erholsame Pausen. + +Febrü verbindet dafür klassische Arbeitsplatzmöbel mit wohnlichen Loungeelementen, mobilen Multifunktionslösungen, Stauraum, Akustik und flexibler Raumzonierung. So entsteht nicht für jede Person derselbe Platz, sondern für jede Tätigkeit der richtige. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Das Wir und das Ich +> +> Ja, ja – das könnte man unter dem üblichen Verkaufs-Blabla abheften. Dahinter steht aber eine einfache Wahrheit: Verantwortung lässt sich nicht dauerhaft nach außen delegieren. Was bleibt, sind das Wir und das Ich. +> +> Ein Unternehmen muss Bedingungen schaffen, unter denen Menschen gemeinsam gut arbeiten, sich austauschen und konzentrieren können. Der Einzelne erhält die Möglichkeit – und trägt auch die Verantwortung –, daraus das Beste zu machen. +> +> Räume und Möbel schaffen allein noch keine gute Unternehmenskultur. Schlechte Arbeitsbedingungen können eine gute Kultur aber jeden Tag unnötig erschweren. +> +> Das Unternehmen stärkt das Wir. Der Einzelne macht daraus das Beste. + +--- + +## Ruhe und Flexibilität sind planbar. + +Akustik, mobile Elemente und flexible Raumlösungen helfen dabei, Bereiche für Kommunikation, Konzentration und Rückzug zu schaffen – und sie später an veränderte Teams und Arbeitsweisen anzupassen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Akustik und flexible Raumstrukturen +> +> Hallende Räume, Telefongespräche und Videokonferenzen können Konzentration und Sprachverständlichkeit erheblich beeinträchtigen. Akustikpaneele, Trennwände, Screens und entsprechend geplante Möbel helfen, Schall zu reduzieren. +> +> Mit modularen Möbeln und Raum-in-Raum-Lösungen lassen sich Arbeitsbereiche neu ordnen, ohne bei jeder Veränderung gleich feste Wände bauen zu müssen. Das ist im großen Büro wichtig – und im privaten Zuhause häufig noch wichtiger. + +--- + +## Gestaltung, die zur Identität passt + +Ein Büro ist ein sichtbarer Teil der Unternehmenskultur. Farben, Materialien und Raumwirkung beeinflussen, wie Mitarbeitende, Kunden und Bewerber ein Unternehmen wahrnehmen. Im Homeoffice muss der Arbeitsplatz professionell funktionieren und sich zugleich in die vorhandene Einrichtung einfügen. + +Dank der hohen Fertigungstiefe bietet Febrü eine große Auswahl an Materialien, Dekoren, Stoffen und Gestellfarben. Corporate Design muss nicht am Briefpapier enden. Und ein Homeoffice muss nach Feierabend nicht wie ein kleines Finanzamt aussehen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Und ja: Pflanzen gehören dazu +> +> Eine Pflanze macht aus einem Unternehmen noch kein nachhaltiges Unternehmen. Sie macht aus einem technisch eingerichteten Raum aber häufig erst einen Ort, an dem Menschen gerne arbeiten. Das hat nichts mit Greenwashing zu tun, sondern schlicht mit Atmosphäre und Stil. + +--- + +## Nachhaltigkeit, die länger als bis zum nächsten Umzug hält + +Nachhaltigkeit beginnt bei der Lebensdauer eines Möbelstücks. Viele Febrü-Möbel sind modular aufgebaut, sodass Komponenten ausgetauscht oder ergänzt werden können, statt bei veränderten Anforderungen das gesamte Möbelstück zu ersetzen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Konkrete Standards statt allgemeiner Versprechen +> +> Febrü produziert mit hoher Fertigungstiefe direkt in Herford. Das sorgt für kurze Wege und kontrollierte Prozesse. Mit laserTEC entstehen hochwertige, fugenlose Kanten ohne zusätzlichen Klebstoff. +> +> Febrü arbeitet nach ISO 9001 und ISO 14001 und verfügt über einen PEFC-Produktkettennachweis. Ausgewählte Febrü-Tischsysteme und Stauraummöbel tragen den Blauen Engel. +> +> Nachhaltig ist nicht das Möbelstück mit dem längsten Werbetext. Nachhaltig ist das Möbelstück, das viele Jahre funktioniert und veränderte Anforderungen mitmacht. + +--- + +## Warum Febrü bei uns? + +Bei uns beginnt eine Planung nicht mit einem Möbelkatalog, sondern mit den Menschen und der Frage, wie tatsächlich gearbeitet wird. Wo ist Konzentration gefragt? Wo entstehen Gespräche? Welche Bereiche benötigen Diskretion? Und wie viel Stauraum wird wirklich gebraucht? + +Wir verbinden Ergonomie, Laufwege, Akustik, Stauraum, Beleuchtung, Kommunikation und Rückzug zu einem Gesamtkonzept. Es geht nicht darum, möglichst viele Möbel in einen Raum zu stellen. Es geht darum, eine bessere Arbeitswelt zu schaffen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Im Homeoffice stellen sich andere Fragen +> +> Wie sichtbar darf der Arbeitsplatz sein? Wo verschwinden Technik und Unterlagen nach Feierabend? Und wie lässt sich professionelles Arbeiten so in die vorhandene Einrichtung integrieren, dass das Zuhause ein Zuhause bleibt? +> +> Auch hier gilt: Ein einzelner Arbeitsplatz ist keine kleinere Aufgabe. Er braucht nur eine andere Antwort als ein vollständiger Bürotrakt. + +--- + +## Erleben statt nur aus dem Katalog auswählen + +Durch die Nähe zwischen Bielefeld und Herford können Febrü-Produkte nach Abstimmung persönlich erlebt werden. Wir organisieren und begleiten den Besuch der Ausstellung – für einen Schreibtisch genauso wie für einen vollständigen Unternehmensstandort. + +Dort lassen sich Materialien anfassen, Stühle und Tische ausprobieren, Akustiklösungen erleben und unterschiedliche Arbeitswelten vergleichen. Sie müssen sich nicht allein auf ein Verkaufsgespräch verlassen. + +--- + +## Abschluss + +**Ihr Arbeitsplatz. Persönlich geplant.** + +Ob Homeoffice, Praxis, Kanzlei, wachsendes Unternehmen oder vollständige Arbeitswelt: Wir planen jeden Arbeitsplatz mit demselben Anspruch. Nur der Umfang verändert sich. + +Gemeinsam entwickeln wir eine Arbeitsumgebung, die gesundes Arbeiten unterstützt, gestalterisch überzeugt und den Menschen zeigt, dass ihre tägliche Leistung wahrgenommen und wertgeschätzt wird – persönlich geplant bei B2in in der Rathausstraße 11 in Bielefeld. + +**Ob im Kleinen oder im Großen – die Detailtreue entscheidet.** + +--- + +## Interne Quellenhinweise (nicht veröffentlichen) + +Die Quellen dienen der Umsetzung, den Herstellern und einer abschließenden rechtlichen Prüfung. Aussagen nur im belegten Umfang veröffentlichen. + +- Febrü, Nachhaltigkeit und Zertifizierungen: https://www.februe.de/unternehmen/nachhaltigkeit +- Blauer Engel, Febrü: https://www.blauer-engel.de/de/unternehmen/februe-bueromoebel-gmbh +- Blauer Engel listet für Febrü zwei Produktgruppen: ausgewählte Tischsysteme sowie ausgewählte Stauraummöbel. **Produktbezug bei Veröffentlichung zwingend beibehalten** – keine pauschale Aussage über das Gesamtsortiment. +- Die pauschale Aussage zur nahezu vollständigen Wiederverwertbarkeit von Aluminium und Stahl wurde entfernt. + +**Vor Veröffentlichung:** +- Herstellerfreigabe einholen +- Umweltaussagen mit Blick auf die EmpCo-Regeln (ab 27.09.2026) prüfen lassen + +--- + +## Änderungen gegenüber der Fassung vom 31.08. + +- Der erklärende Satz „Die Aussage bezieht sich ausdrücklich auf die offiziell gelisteten Produktgruppen und nicht pauschal auf das gesamte Sortiment" wurde aus dem Kundentext entfernt. Die Aussage davor ist bereits präzise; der Hinweis steht jetzt in den internen Quellenhinweisen. +- Die Formulierung zu teilzeitbeschäftigten Müttern bleibt unverändert. diff --git a/dev/2026-08-28/Rotpunkt_Kuechen_Webseitentext_B2in_FINAL.pdf b/dev/2026-08-28/Rotpunkt_Kuechen_Webseitentext_B2in_FINAL.pdf new file mode 100644 index 00000000..45707d98 Binary files /dev/null and b/dev/2026-08-28/Rotpunkt_Kuechen_Webseitentext_B2in_FINAL.pdf differ diff --git a/dev/2026-08-28/Rotpunkt_Webseitentext_FINAL.md b/dev/2026-08-28/Rotpunkt_Webseitentext_FINAL.md new file mode 100644 index 00000000..c684d829 --- /dev/null +++ b/dev/2026-08-28/Rotpunkt_Webseitentext_FINAL.md @@ -0,0 +1,164 @@ +# B2in Interior · Rotpunkt Küchen + +**Küchen, die Haltung zeigen.** + +Webseitentext und Umsetzungsstruktur für die Rotpunkt-Seite +Stand: 31. August 2026 · Korrigierte Fassung zur Umsetzung + +--- + +## Umsetzungshinweise (nicht veröffentlichen) + +**Webstruktur:** Der normale Seitentext bleibt direkt sichtbar. Alle als `AUFKLAPPBARER DETAILBEREICH` gekennzeichneten Vertiefungen werden als Akkordeon umgesetzt. So bleibt die Seite schnell erfassbar, ohne dass Inhalt verloren geht. + +**CTA:** Kein zweiter Button. Das bereits vorhandene schwebende Terminanfrage-Widget erhält auf dieser Markenseite die Beschriftung **„Persönliche Küchenberatung vereinbaren"**. Es bleibt beim Scrollen sichtbar und darf auf dem Smartphone keine Inhalte verdecken. + +**Zu prüfen:** Showroom-Adresse am Seitenende – nennt die Rotpunkt-Seite den richtigen Standort (Küchenstudio), oder ist die Rathausstraße 11 hier korrekt? + +--- + +## Hero + +Eine Küche ist weit mehr als ein funktionaler Raum. Sie ist Treffpunkt, Arbeitsplatz und Mittelpunkt des täglichen Lebens. Und Hand aufs Herz: Wo finden die besten Gespräche statt – und wo enden die meisten Partys? Meistens in der Küche. + +Rotpunkt verbindet außergewöhnliche Gestaltungsfreiheit, intelligente Stauraumlösungen und durchdachte Materiallösungen zu Küchen, die nicht nur gut aussehen, sondern dauerhaft zum Leben ihrer Besitzer passen. + +Unsere Aufgabe ist es, genau herauszuarbeiten, welche Lösung zu Ihnen, Ihrem Alltag und Ihren persönlichen Vorstellungen passt. Bei uns in der Bielefelder Rathausstraße entsteht daraus nicht einfach nur eine Küche. Auf Wunsch denken wir das ganze Zuhause mit und ergänzen es bei Bedarf bis ins Detail handwerklich. + +**Ihre Küche. Persönlich geplant.** + +--- + +## Aus Ostwestfalen. Für Ihr Zuhause. + +Rotpunkt entwickelt und fertigt seit 1930 hochwertige Küchenmöbel an den ostwestfälischen Standorten Bünde und Preußisch Oldendorf-Getmold. + +Regionale Fertigung, jahrzehntelange Erfahrung und die unmittelbare Nähe zum Hersteller sind für uns keine romantischen Randnotizen, sondern die Grundlage für verlässliche Qualität und persönliche Küchenplanung. + +--- + +## Gestaltungsfreiheit bis ins Detail + +Rotpunkt bietet eine außergewöhnliche Vielfalt an Fronten, Farben, Materialien, Korpusdekoren, Arbeitsplatten, Griffvarianten und grifflosen Lösungen. Mehr als 20 beidseitig ausgeführte Korpusfarben sorgen dafür, dass eine Küche nicht nur außen, sondern auch innen konsequent gestaltet werden kann. + +So planen wir keinen vorgegebenen Küchenblock, sondern eine Lösung, die sich an Raum, Architektur und den tatsächlichen Gewohnheiten ihrer Besitzer orientiert. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### FENIX und authentische Holzoptiken +> +> FENIX ist eine besonders matte Oberfläche mit geringer Lichtreflexion, angenehm weicher Haptik und ausgeprägten Anti-Fingerprint-Eigenschaften. Oberflächliche Mikrokratzer können thermisch behandelt werden. 27 aufeinander abgestimmte Farben eröffnen eine breite Gestaltungsvielfalt. +> +> Auch bei ausgewählten Fronten in Holzoptik zeigt sich die Liebe zum Detail. Synchronporen-Dekore stimmen sichtbare Maserung und fühlbare Struktur präzise aufeinander ab. Dadurch entsteht bereits bei pflegeleichten Melamin-Holzreproduktionen eine natürliche Wirkung. +> +> Durch eine sorgfältige Planung kann die Holzoptik über mehrere Fronten hinweggeführt werden. Das Ergebnis ist ein ruhiges, zusammenhängendes Frontbild – ohne dass dafür zwingend Echtholz eingesetzt werden muss. + +--- + +## Mehr Raum. Mehr Ergonomie. Mehr Küche. + +Besonders hohe Unterschränke, niedrige Sockelhöhen und zusätzliche Schrankbreiten schaffen mehr nutzbaren Stauraum und eröffnen neue planerische Möglichkeiten. Klingt nach Kleinkram? Vielleicht. Im täglichen Gebrauch macht genau dieser Kleinkram einen erstaunlich großen Unterschied. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### NEW DIMENSIONS +> +> Mit dem Maßkonzept NEW DIMENSIONS sind beispielsweise Unterschrankhöhen von bis zu 910 Millimetern in Verbindung mit einem nur 50 Millimeter hohen Sockel möglich. +> +> Zusätzliche Schrankbreiten und vielfältige Innenausstattungen helfen uns, den vorhandenen Raum konsequent und ergonomisch zu nutzen. Die Küche wirkt dadurch nicht nur großzügiger – sie ist es tatsächlich. + +--- + +## Nachhaltigkeit mit konkreter Substanz + +Nachhaltigkeit braucht keine großen Versprechen, sondern nachvollziehbare Fakten. Greenline steht bei Rotpunkt für konkrete Materialien, Zertifizierungen und messbare Unterschiede. Deshalb legen wir offen, welche Materialien Rotpunkt verwendet, welche Zertifizierungen dahinterstehen und welchen konkreten Unterschied Greenline für Umwelt und Raumklima macht. + +Die Greenline BioBoard Gen2 besteht aus bis zu 90 Prozent recyceltem Holz. 95 Prozent des Rotpunkt-Küchensortiments sind FSC-zertifiziert. Außerdem sind die Produkte nach JIS F4Star hinsichtlich ihrer Formaldehydemissionen zertifiziert. + +Sie haben bereits eine vergleichbare Küche im Blick, die beim Thema Nachhaltigkeit mehr verspricht? Bringen Sie die Unterlagen gerne mit. Wir schauen gemeinsam genau hin – ehrlich, nachvollziehbar und ohne Verkaufsnebel. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Zahlen, Zertifizierungen und Raumklima +> +> Nach Angaben von Rotpunkt bleibt durch den Einsatz der Greenline-Spanplatten jährlich eine Waldfläche von ungefähr 50 Fußballfeldern erhalten. +> +> Die Greenline BioBoard Gen2 gibt laut Rotpunkt rund 50 Prozent weniger Formaldehyd an die Raumluft ab als herkömmliche Produkte. Das ist insbesondere für ein möglichst gesundes Raumklima relevant. +> +> Die FSC-Zertifizierung wird unter der Kennung FSC C131864 geführt. Entscheidend ist für uns nicht das grüne Etikett, sondern die konkrete und überprüfbare Aussage dahinter. + +--- + +## Mehr als Küchen + +Die Möglichkeiten von Rotpunkt enden nicht bei der klassischen Küche. Das Möbel- und Planungssystem lässt sich auch für Hauswirtschaftsräume, Ankleiden, Badezimmer und weitere Wohnbereiche einsetzen. + +Materialien, Farben und Gestaltungselemente können über mehrere Räume hinweg aufgegriffen werden. So entsteht nicht in jedem Zimmer eine neue Idee, sondern ein zusammenhängendes Interior-Konzept mit einer gemeinsamen gestalterischen Sprache. + +Zusammen mit CABINET, Febrü und weiteren ausgewählten Partnern betrachten wir bei Bedarf den gesamten Innenraum – von Möbeln und Beleuchtung bis zu Tapeten, Fußböden und, wenn es sein soll, der letzten Blume. + +--- + +## Warum eine Rotpunkt-Küche bei uns? + +Bei uns beginnt eine neue Küche nicht mit einem Prospekt, sondern mit Ihrem Raum und der Frage, wie Sie darin leben möchten. + +Arbeitsabläufe, Ergonomie, Stauraum, Materialien, Beleuchtung, Geräte und angrenzende Wohnbereiche planen wir als zusammenhängendes Konzept. Auch die Partystellplätze wollen wohlüberlegt sein – also jene Orte, an denen Gäste später stehen, sitzen, reden und vielleicht etwas länger bleiben. + +Wir stellen nicht einfach möglichst viele Produkte zusammen. Wir entwickeln eine Küche, die gestalterisch, funktional und wirtschaftlich überzeugt – und begleiten den gesamten Weg von der ersten Idee bis zur professionellen Montage. + +Dabei planen wir nicht für den nächsten schnellen Verkauf, sondern für eine möglichst lange und zufriedene Nutzung. Das ist kurzfristig vielleicht nicht immer die wirtschaftlichste Entscheidung für uns. Für eine ehrliche und dauerhafte Kundenbeziehung ist es die einzig richtige. + +--- + +## Erleben statt nur erzählen + +Eine gute Entscheidung entsteht nicht allein am Planungstisch. Auf Wunsch – und manchmal auch mit etwas freundlicher Überzeugungsarbeit – begleiten wir Sie nach Abstimmung in das nahe Rotpunkt-Werk oder in ausgewählte Ausstellungen der Gerätehersteller. + +Dort können Sie Materialien, Fertigung und Technik selbst erleben, Geräte ausprobieren und Fragen direkt vor Ort stellen. Sie müssen sich also nicht allein auf unser Verkaufsgespräch verlassen, sondern erhalten Informationen aus erster Hand, können vergleichen und anschließend mit einem sicheren Gefühl entscheiden. + +Wir begleiten Sie dabei, ordnen die unterschiedlichen Möglichkeiten ein und führen Ihre Entscheidungen zu einem stimmigen Gesamtkonzept zusammen. + +--- + +## Küchenstudio plus Handwerk + +Rotpunkt deckt bereits eine enorme planerische und gestalterische Bandbreite ab. Es geht deshalb ausdrücklich nicht um Sonderanfertigungen um jeden Preis. + +Natürlich gibt es trotzdem immer diesen einen planerischen Sonderfall, bei dem selbst hervorragende Standardsysteme an ihre Grenzen kommen. Dann greift unsere dauerhaft verankerte Schnittstelle zu angeschlossenen Tischlereien. + +Wir wägen gestalterischen Mehrwert, technische Notwendigkeit und wirtschaftliche Sinnhaftigkeit ehrlich miteinander ab. Am Ende entscheiden aber Ihre Wünsche – nicht unser Drang, Ihnen unbedingt noch eine Sonderlösung zu verkaufen. + +> **AUFKLAPPBARER DETAILBEREICH** +> ### Wenn das Handwerk den letzten Millimeter übernimmt +> +> Möglich sind beispielsweise passgenaue Nischenlösungen, besondere Verkleidungen, raumhohe Einbauten, Übergänge zu angrenzenden Möbeln oder individuelle Anpassungen an die vorhandene Architektur. +> +> Bei Bedarf endet das Konzept nicht an der Arbeitsplatte. Auch Tapeten, Fußböden, Wandgestaltung, Beleuchtung und weitere Details können einbezogen werden. + +**Die Möglichkeiten eines modernen Küchenstudios. Die Flexibilität des Handwerks.** + +--- + +## Abschluss + +**Ihre Küche. Persönlich geplant.** + +Jeder Raum ist anders. Jeder Alltag stellt andere Anforderungen. Gemeinsam entwickeln wir eine Rotpunkt-Küche, die gestalterisch überzeugt, im Alltag funktioniert und dauerhaft zu Ihnen passt. Persönlich geplant bei B2in in der Rathausstraße 11 in Bielefeld. + +--- + +## Interne Quellenhinweise (nicht veröffentlichen) + +Die Quellen dienen der Umsetzung, den Herstellern und einer abschließenden rechtlichen Prüfung. Aussagen nur im belegten Umfang veröffentlichen. + +- Rotpunkt Küchen, Nachhaltigkeit: https://www.rotpunktkuechen.de/nachhaltigkeit +- Dort belegt: bis zu 90 % Recyclingholz, ca. 50 Fußballfelder, 95 % FSC (FSC C131864), JIS F4Star, rund 50 % weniger Formaldehyd bei Greenline BioBoard Gen2 + +**Vor Veröffentlichung:** +- Herstellerfreigabe einholen +- Waldflächen-Aussage nur mit ausdrücklicher Bestätigung verwenden +- Umweltaussagen mit Blick auf die EmpCo-Regeln (ab 27.09.2026) prüfen lassen + +--- + +## Änderungen gegenüber der Fassung vom 31.08. + +- Die Abschnitte „Nachhaltigkeit mit konkreter Substanz" und „Greenline hält dem Vergleich stand" wurden zusammengeführt; der doppelte Satz „Nachhaltigkeit braucht keine großen Versprechen, sondern nachvollziehbare Fakten" steht nur noch einmal. diff --git a/dev/2026-08-28/deploy/dateiliste.txt b/dev/2026-08-28/deploy/dateiliste.txt new file mode 100644 index 00000000..3a1a29b9 --- /dev/null +++ b/dev/2026-08-28/deploy/dateiliste.txt @@ -0,0 +1,126 @@ +.env.example +.gitignore +app/Helpers/ThemeHelper.php +app/Http/Controllers/DisplayEmbedController.php +app/Livewire/Admin/Cms/CabinetInfoTablet.php +app/Livewire/Web/Components/Sections/AudienceSwitch.php +app/Livewire/Web/Components/Sections/BrandDetail.php +app/Livewire/Web/Components/Sections/BrandOverview.php +app/Livewire/Web/Components/Sections/CoreBrands.php +app/Livewire/Web/Components/Sections/DisplayFeed.php +app/Livewire/Web/Components/Sections/ImmobilienTeaser.php +app/Livewire/Web/Components/Sections/MagazinDetail.php +app/Livewire/Web/Components/Sections/MagazinList.php +app/Livewire/Web/Components/Sections/MarkenCta.php +app/Livewire/Web/Components/Sections/MarkenGrowth.php +app/Livewire/Web/Components/Sections/MarkenNetwork.php +app/Livewire/Web/Components/Sections/PageHero.php +app/Livewire/Web/Components/Sections/ReferenceProjects.php +app/Livewire/Web/Components/Sections/Sponsoring.php +app/Livewire/Web/Components/UI/AnnouncementBar.php +app/Livewire/Web/Components/UI/AppointmentWidget.php +app/Livewire/Web/Components/UI/Footer.php +app/Models/CmsArticle.php +app/Models/CmsBrand.php +app/Providers/ThemeServiceProvider.php +app/Support/DisplayModuleSettings.php +app/helpers.php +config/app.php +config/cms_section_map.php +config/seeding.php +database/factories/CmsBrandFactory.php +database/migrations/2026_08_28_110407_create_cms_brands_table.php +database/migrations/2026_08_28_112244_add_summary_and_highlights_to_cms_brands_table.php +database/migrations/2026_09_03_084254_add_page_sections_to_cms_brands_table.php +database/seeders/BrandGalleryCaptionNormalizerSeeder.php +database/seeders/BrandLogoSeeder.php +database/seeders/BrandPageTextSeeder.php +database/seeders/CmsArticleSeeder.php +database/seeders/CmsBrandSeeder.php +database/seeders/CmsContentSeeder.php +database/seeders/DatabaseSeeder.php +database/seeders/DemoBrandGallerySeeder.php +database/seeders/DisplayInteriorBrandingSeeder.php +database/seeders/DisplayMediaUrlNormalizerSeeder.php +database/seeders/InteriorRelaunchCmsSeeder.php +public/_cabinet/assets/b2in-interior-logo-negative.svg +public/_cabinet/assets/b2in-interior-logo-positive.svg +public/_cabinet/display/index.html +public/img/assets/marken/cabinet.svg +public/img/assets/marken/februe.png +public/img/assets/marken/rotpunkt.svg +public/img/assets/sponsoring/arminia-bielefeld.png +public/img/assets/sponsoring/tc-bad-waldliesborn-horn.jpg +public/img/assets/sponsoring/tc-hoevelhof.png +public/img/logos/b2in-interior-logo-negative.png +public/img/logos/b2in-interior-logo-negative.svg +public/img/logos/b2in-interior-logo-positive.png +public/img/logos/b2in-interior-logo-positive.svg +resources/css/web/shared-styles.css +resources/lang/de/b2in.php +resources/lang/de/ui.php +resources/lang/en/b2in.php +resources/lang/en/ui.php +resources/views/livewire/admin/cms/brands-index.blade.php +resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php +resources/views/livewire/admin/cms/content-index.blade.php +resources/views/livewire/admin/cms/dashboard.blade.php +resources/views/livewire/admin/cms/projects-index.blade.php +resources/views/livewire/web/components/sections/audience-switch.blade.php +resources/views/livewire/web/components/sections/brand-detail.blade.php +resources/views/livewire/web/components/sections/brand-overview.blade.php +resources/views/livewire/web/components/sections/core-brands.blade.php +resources/views/livewire/web/components/sections/display-feed.blade.php +resources/views/livewire/web/components/sections/f-a-q.blade.php +resources/views/livewire/web/components/sections/immobilien-teaser.blade.php +resources/views/livewire/web/components/sections/magazin-list.blade.php +resources/views/livewire/web/components/sections/marken-cta.blade.php +resources/views/livewire/web/components/sections/marken-growth.blade.php +resources/views/livewire/web/components/sections/marken-network.blade.php +resources/views/livewire/web/components/sections/page-hero.blade.php +resources/views/livewire/web/components/sections/reference-projects.blade.php +resources/views/livewire/web/components/sections/sponsoring.blade.php +resources/views/livewire/web/components/ui/announcement-bar.blade.php +resources/views/livewire/web/components/ui/appointment-widget.blade.php +resources/views/livewire/web/components/ui/footer.blade.php +resources/views/livewire/web/components/ui/header.blade.php +resources/views/vendor/mail/html/header.blade.php +resources/views/web/about.blade.php +resources/views/web/contact.blade.php +resources/views/web/cookie-policy.blade.php +resources/views/web/faq.blade.php +resources/views/web/home.blade.php +resources/views/web/impressum.blade.php +resources/views/web/layouts/web-master.blade.php +resources/views/web/magazin-detail.blade.php +resources/views/web/magazin.blade.php +resources/views/web/marken-show.blade.php +resources/views/web/marken.blade.php +resources/views/web/netzwerk.blade.php +resources/views/web/privacy.blade.php +resources/views/web/referenzen.blade.php +resources/views/web/terms.blade.php +routes/admin.php +routes/web.php +tests/Feature/Admin/Cms/CmsBrandsIndexTest.php +tests/Feature/AnnouncementBarTest.php +tests/Feature/AppointmentWidgetTest.php +tests/Feature/BrandPageSectionsTest.php +tests/Feature/BrandPageTest.php +tests/Feature/CabinetInfoTabletTest.php +tests/Feature/Cms/CmsArticleTest.php +tests/Feature/Cms/CmsBrandTest.php +tests/Feature/Cms/CmsContentSeederTest.php +tests/Feature/DisplayInteriorBrandingTest.php +tests/Feature/DisplayMediaUrlNormalizerTest.php +tests/Feature/DisplayVersionApiTest.php +tests/Feature/DisplayVersionTest.php +tests/Feature/DisplayWebFeedTest.php +tests/Feature/FooterTest.php +tests/Feature/HomePageStructureTest.php +tests/Feature/InteriorPageTest.php +tests/Feature/LiveSiteReviewTest.php +tests/Feature/MagazinPageTest.php +tests/Feature/MarkenPageTest.php +tests/Feature/PreviewHostTest.php +tests/Feature/ReferenzenPageTest.php diff --git a/dev/2026-08-28/entwicklungsplan.md b/dev/2026-08-28/entwicklungsplan.md new file mode 100644 index 00000000..f389f811 --- /dev/null +++ b/dev/2026-08-28/entwicklungsplan.md @@ -0,0 +1,2234 @@ +# Entwicklungsplan: B2in Interior – Neuausrichtung Website & CMS + +**Erstellt:** 28.08.2026 · zuletzt aktualisiert 28.08.2026 +**Basis:** `B2in_Interior-Konzept.md`, `umsetzungliste.md`, `umsetzung-inhalte.md` +**Vorgänger:** `dev/2026-02-27/b2in-website-umsetzungsplan.md` +**Rahmen:** 37–39 Std. freigegeben (18 + 8 + 4 + 3 + 4–6) +**Status:** Revision 14 – **AP0–AP6 umgesetzt**, Seite abgestimmt, Markentexte Rotpunkt und Febrü +eingearbeitet. **Arbeitsplan 14.09. (`umsetzungen-14-09-2026.md`), Punkt 1.1 Farbwelt und Logo +umgesetzt (Nachtrag 6).** Offen: Bildmaterial, Rechtstexte und die Herstellerfreigaben. +**Umgebung:** Dev Container, `php artisan …` (nicht `vendor/bin/sail …`) + +> **Verbindliche Schreibweisen:** **Interior** (nicht „Interieur"), **Febrü**, **Rotpunkt Küchen**, **CABINET**. +> Das Konzeptpapier schreibt durchgehend „Interieur" – die Umsetzungsliste überschreibt das. Gilt für Code, +> Texte, Commit-Messages und Datenbank-Inhalte. + +--- + +## 0. Stand & nächste Schritte + +**Stand 03.09.2026:** T1 und B1–B13 abgearbeitet, **alle Arbeitspakete AP0–AP6 umgesetzt**, die +Bestandsinhalte auf den Interior-Auftritt umgestellt, die Seite abgestimmt und alle vier Punkte +der Abstimmungsrunde vom 03.09. umgesetzt (Nachtrag 5). Alle drei Markenseiten haben jetzt einen +ausführlichen Text. `php artisan test --compact`: **873 grün** (3217 Assertions), Pint sauber, +`npm run build:b2in` gebaut. +**Die Seite ist testbereit** – die ausstehenden Zulieferungen trägt Marcel selbst im CMS nach. + +| Paket | Stand | +|---|---| +| AP0 Vorbereitung | ✅ | +| AP1 Umstrukturierung & Umtextung | ✅ | +| AP2 Kernmarken-Modul + `/marken` + Galerie | ✅ | +| AP3 Terminanfrage-Widget + WhatsApp-Link | ✅ (Nummer fehlt noch) | +| AP6 Sponsoring | ✅ | +| Bestandsinhalte FAQ / About / Kontakt / Magazin | ✅ | +| Markenseiten Rotpunkt und Febrü (Zulieferung 31.08.) | ✅ | +| Abstimmungsrunde 03.09.: korrigierte Texte, CABINET-Seite, Galerie-Hinweise, Abschluss-CTA | ✅ | +| AP4 Display-Spiegelung | ✅ | +| AP5 Rubrik-Seite „Referenzen" | ✅ | +| Arbeitsplan 14.09. · 1.1 Farbwelt, Logo je Welt, Welten-Wechsler | ✅ (Nachtrag 6) | +| Welten-Trennung: Navigation, Magazin, Über uns, FAQ, Widget je Welt | ✅ (Nachtrag 7, Erweiterung zu 1.1) | +| Arbeitsplan 14.09. · 1.2 Tischlerei als vierte Marke | ✅ Otterpohl mit Text und Logo (Nachtrag 9 + 15) | +| Arbeitsplan 14.09. · 1.4 Akkordeon und Widget-Beschriftung je Marke | ✅ (bereits seit 03.09., CABINET-Text fehlt im CMS) | +| Arbeitsplan 14.09. · 1.3 Herkunfts-Sektion „Aus der Region gefertigt“ | ✅ (Nachtrag 10 + 11: Standorte an der Marke, Ablauf CABINET von Marcel bestätigen lassen) | +| Abstimmung 23.09. · WhatsApp zuerst im Widget, Business-Nummer, Farbvariante „Modern“ mit Schalter | ✅ (Nachtrag 18, Palette von Marcel entscheiden lassen) | +| Briefing 24.09. · Entwurf Startseite „Greige“ mit Gold-Logo, Hero-Slideshow, Projekttext | ✅ Entwurf unter /entwurf (Nachtrag 19); offen: Foto Marcel, Slides selbst pflegen, Logo-Verläufe | +| Zulieferung 24.09. · Otterpohl Text v2, Bilder (Werkstatt + Referenzen) | ✅ (Nachtrag 19, Unterschriften und Referenzen-Fallback von Marcel bestätigen lassen) | + +### Nächste Schritte + +**Vor dem Versand des Testlinks — nichts.** Alle Seiten laden, die Suite ist grün, die Assets sind +gebaut. Was fehlt, ist Material, kein Code. + +**Was Marcel selbst nachträgt (alles im CMS, ohne Codeänderung):** + +| Was | Wo im Admin | +|---|---| +| Stimmungsbilder der drei Marken (Logos liegen seit 03.09. vor) | Kernmarken → Marke bearbeiten | +| Echte Projektfotos statt der Demobilder (siehe Warnung unten) | Kernmarken → Bildergalerie | +| Herstellerseiten-URLs | Kernmarken → Herstellerseite | +| Motiv für den Kopf von `/marken` | Inhalte → `marken` → `hero` | +| WhatsApp-Nummer – seit Nachtrag 18 per Seeder gesetzt (0521-98620100), Änderung weiter hier | Inhalte → `global` → `contact` → `whatsapp` | +| Welches Display auf der Website läuft | Inhalte → `referenzen` → `display_feed` → `display_id` | + +**Beim Deployment mit erledigen (Abschnitt 8):** + +1. **Rechtstexte** – AGB und Datenschutzerklärung beschreiben weiter das alte Geschäft + („Connecting Design and Property", Dubai/Lissabon, Supply-Chain-Management, + `resources/lang/de/b2in_legal.php` Zeilen 117 und 221). Marcel lässt die Formulierung prüfen, + danach eintragen. Der WhatsApp-Absatz kommt im selben Zug dazu. +2. **OG-Bild** – das Layout verweist auf `img/assets/b2in-og-default.jpg`, die Datei existiert + nicht. Bis dahin zeigt jede geteilte Seite eine Vorschau ohne Bild. +3. **Seeder-Passwort rotieren** (B8) – steht weiter in der Git-Historie. +4. Die zwei Magazin-Beiträge und die Sponsoren-Kurztexte gegenlesen lassen. +5. **CABINET-Seitentext freigeben lassen** – für CABINET lag keine Zulieferung vor, der Text ist + von uns nach dem Muster der beiden abgestimmten Seiten verfasst (Nachtrag 5). +6. **Demobilder der Rubrik „Realisiertes Projekt" ersetzen.** Seit dem Entfallen der + „BEISPIEL"-Kennzeichnung geben sie sich als echte Referenzen aus. + +**Danach offen, aber nicht Teil dieses Auftrags:** YouTube-Einbindung mit Consent-Gate auf der +Referenzen-Seite (Abschnitt 4). Sinnvoll, sobald es Videos gibt – vorher wäre es ein Consent-Gate +ohne Inhalt. + +--- + +## 1. Ist-Architektur (relevanter Ausschnitt) + +### 1.1 Content-Pipeline + +``` +resources/lang/de/b2in.php (themes.b2in.
) ← Quelltexte / Fallback + ├─ config/cms_section_map.php section → [group, key] + ├─ database/seeders/CmsContentSeeder.php + ▼ +flux_cms_contents (group, key, type, value:JSON, translatable de/en) + ├─ FluxCms\Core\Services\CmsContentService (Cache: flux_cms.content.__all__) + ▼ +cms_theme_section('hero') → CMS gewinnt, sonst trans('b2in.themes.b2in.hero') +``` + +Redaktion unter `admin/flux-cms/content` → [`content-index.blade.php`](../../resources/views/livewire/admin/cms/content-index.blade.php) +(Volt, 782 Z.): Group/Key-Editor mit JSON-Repeater-Modal, Medien-Picker, Flux-Editor für HTML. +**Kann Werte ändern, aber keine Sektion neu anlegen.** + +### 1.2 Seiten & Sektionen + +- Startseite: `routes/web.php` → `resources/views/web/home.blade.php` +- Sektionen = Livewire-Komponenten unter `app/Livewire/Web/Components/Sections/`, + Muster `mount() { $this->content = cms_theme_section(''); }` +- Ist-Reihenfolge: `header → hero → founder-bar → content-section(synergie) → vision-section → ecosystem-core → cta-section → footer` +- Navigation und Ankündigungsleiste liegen **bereits im CMS** (`global.header.navigation`, `global.announcement_bar`) + → Umbau ist Redaktion, kein Code. +- SEO: `@section('title')` / `@section('meta_description')` je View; OG-Tags leiten sich + in `web-master.blade.php` automatisch daraus ab → **OG kostet keinen Extra-Aufwand** (aber siehe B6). +- Immobilien rendern statisch aus `resources/lang/de/immobilien-azizi.json` – wie gefordert. + +### 1.3 Display-CMS + +`Display` (Gerät) → `DisplayPlaylist` (draft/published) → `DisplayVersion` („Modul", Enum +`DisplayVersionType`: `video-display` | `b2in` | `offers`) → `DisplayVersionItem`. +[`DisplayPlaylistConfigBuilder`](../../app/Services/DisplayPlaylistConfigBuilder.php) erzeugt das +Player-JSON. Player: `public/_cabinet/display/index.html`, festes 1080×1920-Raster (9:16), per Scale eingepasst. +API vorhanden: `/api/display/{display}/config`, `/api/display/preview/{token}`, +`/api/display/module/{module}/preview` – registriert für Portal-Domain **und** als Fallback für alle Domains. + +### 1.4 Sonstiges + +- Kontaktformular: `packages/acme/contact-form` – **unterstützt bereits `customFields` + Presets, + Honeypot, Rate-Limit und `SpamDetector`.** Das Terminanfrage-Widget kann darauf aufsetzen. +- Tests: Pest, SQLite in-memory, `php artisan test --compact`. + +--- + +## 2. Gefundene Bugs & Optimierungen + +Beim Abgleich der Umsetzungsliste mit dem Code aufgefallen. + +**Stand 28.08.2026: alle Befunde behoben, Suite grün (699 Tests, 2715 Assertions).** + +| # | Befund | Status | +|---|---|---| +| T1 | Info-Tablet: Leeren per X wird nicht gespeichert | ✅ behoben | +| B1 | CMS-Seeder überschreibt redaktionelle Inhalte | ✅ behoben | +| B2 | Magazin-CMS nicht ans Frontend angeschlossen | ✅ behoben (Variante A: Slug-Routen + 301) | +| B3 | Ankündigungsleiste bleibt nach Umtexten unsichtbar | ✅ behoben | +| B4 | Footer hartkodiert, nicht pflegbar | ✅ behoben | +| B5 | Englische Footer-Labels auf deutscher Seite | ✅ behoben | +| B6 | 19 Seiten erben alte Dubai-Beschreibung | ✅ behoben (Fallback + canonical) | +| B7 | Content-Seeder nicht registriert | ✅ behoben | +| B8 | Klartext-Passwort im Seeder | ✅ behoben | +| B9 | Medien-Picker greift bei leeren Logo-Feldern nicht | ✅ behoben | +| B10 | Alt-Begriffe im Seiteninhalt | ✅ behoben in AP1 | +| B12 | `/service` und `/portfolio` lieferten 500 | ✅ behoben (301) | +| B11 | `/magazin/{unbekannt}` liefert 200 statt 404 | ✅ behoben | + +--- + +### T1 · Info-Tablet: Leeren der Öffnungszeiten wird nicht gespeichert ✅ + +Gemeldet am 28.08.2026: „Wenn ich den Inhalt weg-X, kommt nicht Geschlossen. Der Wert kommt +wieder, wenn ich in ein anderes Feld klicke. Setze ich Öffnung und Schluss auf X und speichere, +ist Öffnung wieder der alte Wert." + +**Drei Ursachen, alle in `cabinet-info-tablet.blade.php` bzw. `CabinetInfoTablet.php`:** + +1. **Zwei Submit-Handler auf demselben Formular.** `wire:submit.prevent="save"` **und** + `x-on:submit.prevent="… $wire.save(hours)"` waren gleichzeitig gebunden – `save()` lief pro + Klick zweimal, einmal mit den DOM-Werten und einmal ohne. Welcher Aufruf zuletzt schrieb, + war nicht deterministisch. Das erklärt „speichert nur den letzten". +2. **`wire:model.live` auf den Zeit-Pickern.** Jede Änderung löste einen Roundtrip aus; beim + erneuten Rendern hat Livewire ein zuvor geleertes Feld mit dem unveränderten Serverwert + überschrieben. Das erklärt „der Wert kommt wieder, wenn ich in ein anderes Feld klicke". +3. **Geöffnet/Geschlossen wurde serverseitig gerechnet** und konnte deshalb erst nach einem + Roundtrip umspringen. Das erklärt „es kommt nicht Geschlossen". + +**Behoben:** nur noch ein Submit-Pfad (das DOM ist die Quelle der Wahrheit), `.live` entfernt, +die Geöffnet/Geschlossen-Anzeige rechnet clientseitig per Alpine und reagiert damit sofort auf das X. + +**Zusätzlich:** `save(array $hours)` hat über `property_exists()` **jede** öffentliche Property +aus dem Client-Payload beschreibbar gemacht – auch `storeStatus` und `contactEmail`. Jetzt greift +eine Allowlist auf die 14 Zeitfelder. + +**Tests:** 5 neue Fälle in `tests/Feature/CabinetInfoTabletTest.php` (15 grün) – u. a. mehrere Tage +in einem Submit leeren, nur die Öffnung leeren, Fallback ohne DOM-Payload, Allowlist. + +**Noch am Gerät zu prüfen:** die drei Ursachen liegen im Browser-Verhalten; die Tests sichern den +Serverpfad ab. Bitte im Portal einmal gegenprüfen: mehrere Tage leeren → speichern → neu laden. + +### B1 · Der CMS-Seeder überschreibt alles, was Marcel pflegt ✅ + +`database/seeders/CmsContentSeeder.php:76-90` – bestehende Einträge werden bedingungslos +per `$content->update()` mit den Werten aus der Lang-Datei überschrieben. Geschützt sind nur +die zwei Sektionen in `$skipSections`. + +**Wirkung:** Jeder erneute Seeder-Lauf – auch der geplante nach der Umtextung – setzt sämtliche +redaktionellen Änderungen zurück. Die Zusage „von Marcel selbst pflegbar" hält so nicht. + +**Behoben:** Default ist jetzt „nur anlegen, wenn nicht vorhanden". Überschreiben nur noch über +`$seeder->overwriteExisting = true`. Der Lauf meldet, wie viele Einträge unangetastet blieben, und +leert den CMS-Cache, wenn er etwas geändert hat. +**Tests:** 3 neue Fälle in `tests/Feature/Cms/CmsContentSeederTest.php` – gepflegter Inhalt bleibt, +fehlende Sektionen werden trotzdem angelegt, bewusstes Überschreiben funktioniert. + +### B2 · Das Magazin-CMS ist nicht mit dem Frontend verbunden ✅ + +- Admin `admin/flux-cms/articles` schreibt nach `cms_articles` (Model `CmsArticle`). +- Frontend liest `trans('b2in.articles')`: + `app/Livewire/Web/Components/Sections/MagazinList.php:20` und `MagazinDetail.php:48`. +- `CmsArticle` wird **außerhalb des Admin-Bereichs nirgends verwendet** (verifiziert per Grep). + +**Wirkung 1:** Was Marcel im Magazin-CMS bearbeitet, erscheint nicht auf der Website. +**Wirkung 2 (für dieses Projekt entscheidend):** Die Umsetzungsliste sagt für die Kernmarken +„adaptiert vom **Magazin-Muster**". Das Admin-CRUD ist als Vorlage brauchbar, die +Frontend-Anbindung fehlt dort aber – **dieser Fehler darf nicht mitkopiert werden.** + +**Bei der Umsetzung aufgefallen – das ist kein reiner Verdrahtungsfehler:** +Die Artikel liegen in beiden Quellen (5 in `cms_articles`, 5 in der Lang-Datei, inhaltsgleich), +**aber unter verschiedenen Schlüsseln**: die Lang-Datei nutzt 1–5, die Datenbank die IDs 8–12. +Die öffentlichen URLs lauten heute `/magazin/1` … `/magazin/5`. Eine Umstellung auf das Model +**ändert damit die öffentlichen URLs** und braucht eine Slug-Route plus Weiterleitungen von den +alten numerischen IDs – sonst brechen bestehende Links und die Google-Indexierung. + +**Deshalb bewusst nicht einfach umgestellt.** Entscheidung nötig: + +| Variante | Aufwand | Wirkung | +|---|---|---| +| A · Slug-Routen `/magazin/{slug}` + 301 von den alten IDs | ≈3–4 Std. | sauber, CMS wird wirksam, URLs werden sprechend | +| B · Numerische Route beibehalten, Auflösung über die DB-ID | ≈2 Std. | CMS wird wirksam, aber `/magazin/1` … `/magazin/5` werden zu 404 | +| C · Vorerst so lassen | 0 | Magazin funktioniert, das Magazin-CMS bleibt aber wirkungslos | + +**Umgesetzt: Variante A.** + +- `CmsArticle::forFrontend()` liefert die veröffentlichten Artikel nach Slug indiziert. + Das CMS gewinnt; solange keine Artikel gepflegt sind, greift die Lang-Datei als Fallback – + dieselbe Reihenfolge wie bei `cms_theme_section()`, damit das Magazin auf einer frischen + Installation nicht leer ist. +- Die ID→Slug-Zuordnung lag als private Konstante im `CmsArticleSeeder` und ist jetzt + `CmsArticle::LEGACY_ID_TO_SLUG` – eine Quelle für Seeder, Fallback und Weiterleitung. +- `MagazinList` und `MagazinDetail` lesen aus dem Model statt aus `trans('b2in.articles')`; + die Übersicht verlinkt über `route('magazin.detail')` statt über die numerische ID. +- Neue Route `/magazin/{slug}`. Die alten `/magazin/1` … `/magazin/5` leiten dauerhaft (301) + auf den Slug um; sie stehen per `whereNumber()` **vor** der Slug-Route, sonst würde diese + die Ziffern selbst schlucken. Numerische IDs ohne Zuordnung liefern 404. +- Die Detailseite fragte die Artikel zweimal ab (Artikel + verwandte Artikel) – jetzt gemerkt, + eine Abfrage pro Seitenaufruf. + +**Tests:** `tests/Feature/MagazinPageTest.php` auf 13 Fälle erweitert – Slug-Auslieferung, +301 von jeder alten ID, 404 bei unbekanntem Slug, CMS schlägt Lang-Datei, unveröffentlichte +Artikel sind unsichtbar, Fallback bei leerer Tabelle. `LiveSiteReviewTest` prüft für jede alte +ID die Weiterleitung **und** dass das Ziel lädt. + +⚠️ **Beim Deployment:** `/magazin/1` … `/magazin/5` sind indexiert. Die 301 fängt sie ab, aber +nach dem Livegang bitte einmal in der Search Console gegenprüfen. Externe Links, die noch auf +die alten URLs zeigen, bleiben funktionsfähig. + +**Nebenbefund, nicht angefasst:** `MagazinDetail::$relatedArticles` wird berechnet, aber von +keiner View gerendert – entweder die Sektion „verwandte Artikel" nachziehen oder die Property +entfernen. Deine Entscheidung. + +### B3 · Ankündigungsleiste: alte Wegklicker sehen die neue Leiste nie ✅ + +`announcement-bar.blade.php:2` merkt sich das Wegklicken in +`localStorage['announcement_dismissed_']`. Bleibt die `id` beim Umtexten gleich, bleibt die +Leiste für alle Wiederkehrer unsichtbar. + +**Behoben:** Der Dismiss-Schlüssel wird jetzt aus Inhalt **und** `id` abgeleitet +(`AnnouncementBar::getDismissKeyProperty()`). Wird die Leiste im CMS neu belegt, sehen auch +Wiederkehrer die neue Meldung – ohne dass jemand daran denken muss, die `id` mitzuändern. +**Tests:** 2 neue Fälle in `tests/Feature/AnnouncementBarTest.php`. + +### B4 · Der Footer ist vollständig hartkodiert ✅ + +`footer.blade.php` – Claim „Connecting Design and Property" steht in Zeile 15 im Markup, +`Footer.php` lädt überhaupt keinen Content. Es gibt keine `footer`-Sektion in `cms_section_map.php`. + +**Wirkung:** Sowohl „Footer-Claim ersetzen" als auch „FAQ in den Footer" aus der Umsetzungsliste +erfordern einen kleinen Umbau; Marcel kann den Claim danach ohne CMS-Anbindung weiterhin nicht ändern. +**Behoben:** Neue CMS-Sektion `global.footer` (Claim, Byline, Linkliste) in `cms_section_map.php` +und in beiden Lang-Dateien. `Footer.php` lädt sie über `cms_theme_section('footer')`, die View +rendert Claim und Links daraus und fällt ohne CMS-Eintrag auf sinnvolle Defaults zurück. +Der Claim lautet jetzt „Premium-Einrichtung. Persönlich gemacht.", FAQ ist als erster Link drin. +**Tests:** `tests/Feature/FooterTest.php` (4 Fälle). + +### B5 · Englische Footer-Labels auf der deutschen Seite ✅ + +„Privacy Policy", „Terms of Service", „Cookie Policy" waren hartkodiert englisch, während +„Impressum" korrekt über `__('ui.legal_notice')` lief. +**Behoben:** neue Keys `privacy_policy`, `terms_of_service`, `cookie_policy`, `faq` in +`resources/lang/{de,en}/ui.php`; die deutsche Seite zeigt jetzt Datenschutz / AGB / +Cookie-Richtlinie / Häufige Fragen / Impressum. + +### B6 · 19 Seiten tragen die alte Dubai-Beschreibung ✅ + +`web-master.blade.php:11` und `:18` haben als Fallback fest verdrahtet: +„B2in – Connecting Design and Property. Exklusive Immobilien in Dubai & europäisches Einrichtungsnetzwerk." +19 Views unter `resources/views/web/` setzen **kein** `@section('meta_description')` und erben damit +diesen Text – darunter die Live-Seite `magazin-detail.blade.php`. + +**Behoben:** Der Fallback in `web-master.blade.php` (Description **und** OG-Description) trägt +jetzt den Interior-Text aus `umsetzung-inhalte.md` §0. Zusätzlich `` ergänzt, +das fehlte komplett. +**Offen als B10:** Der Alt-Begriff-Bestand im Seiteninhalt („Supply-Chain", „Local-for-Local", +„Ökosystem", „Investment", „Connecting Design") – **90 Treffer in 14 Dateien**, u. a. der +Hero-`card_text`. Das ist Content-Arbeit mit den freigegebenen Texten und gehört in AP1, nicht +in einen Bugfix. + +### B7 · Content-Seeder sind nirgends registriert ✅ + +`DatabaseSeeder::run()` ruft nur `TestDisplaySeeder`. `CmsContentSeeder`, `CmsArticleSeeder`, +`CmsProjectSeeder`, `CmsLegalSeeder`, `NetzwerkPageCmsSeeder` müssen von Hand aufgerufen werden; +ein frisch aufgesetztes System hat keine CMS-Inhalte. +**Behoben:** `CmsContentSeeder`, `CmsLegalSeeder`, `NetzwerkPageCmsSeeder`, `CmsArticleSeeder` +und `CmsProjectSeeder` sind in `DatabaseSeeder::run()` registriert – nach dem B1-Fix gefahrlos, +weil bestehende Inhalte unangetastet bleiben. + +### B8 · Klartext-Passwort im Seeder ✅ + +`database/seeders/DatabaseSeeder.php` enthielt ein echtes Passwort im Repo. +**Behoben:** Zugangsdaten kommen jetzt aus `config/seeding.php` bzw. `SEED_ADMIN_*` in der `.env`; +`.env.example` ist ergänzt. +⚠️ **Das alte Passwort steht weiterhin in der Git-Historie – bitte rotieren, falls es irgendwo +sonst benutzt wird.** + +### B9 · Medien-Picker greift bei leeren Logo-Feldern nicht ✅ + +`content-index.blade.php` → `_cmsFieldLooksLikeImage()` erkennt Bildfelder an einer festen +Feldnamen-Liste **oder** an der Dateiendung **im Wert**. `logo` steht nicht in der Liste, und ein +noch leeres Feld hat keinen Wert → wird als reines Textfeld gerendert. + +**Wirkung:** Marcel kann bei einer neu angelegten Marke bzw. bei den Sponsoring-Logos das Bild +nicht per Picker setzen, sondern müsste den Dateinamen abtippen. Betrifft AP2 und AP6 direkt. +**Behoben:** `logo` in die Feldnamen-Liste und `_logo$` in die Suffix-Regex aufgenommen. Zusätzlich +prüft `_cmsFieldLooksLikeImage()` den Feldnamen jetzt **vor** dem Wert – ein noch leeres Bildfeld +wird dadurch als Bild erkannt und bekommt den Picker. (`logo_width` bleibt korrekt ein Textfeld.) + +### B11 · `/magazin/{unbekannte-id}` liefert 200 statt 404 ✅ + +`MagazinDetail::loadArticle()` fiel bei unbekannten IDs still auf Artikel 1 zurück +(`$articles[$this->articleId] ?? $articles[1]`). `/magazin/999` lieferte damit Status 200 mit dem +Inhalt von Artikel 1 – beliebig viele URLs mit identischem Inhalt, schlecht für die Indexierung, +und bei fehlendem Artikel 1 ein Fehler statt einer 404. + +**Behoben:** `abort_unless(isset($articles[$this->articleId]), 404)`. +**Tests:** 2 neue Fälle in `tests/Feature/MagazinPageTest.php`. + +--- + +## 3. Arbeitspakete + +Gliederung folgt der Umsetzungsliste. Reihenfolge nach dem Vorschlag aus `umsetzung-inhalte.md`. + +### AP0 · Vorbereitung (im Rahmen von AP1) + +Branch `feature/b2in-interior`, DB-Backup, Backup der Startseite (Muster +`dev/2026-02-27/backup-before-relaunch/`), `php artisan test --compact` als Referenzstand. + +### AP1 · Website-Umstrukturierung & Umtextung — ✅ umgesetzt + +**Global** +- Meta-Title und Description auf Interior umgestellt; fünf Live-Seiten hatten gar keine eigene + Description und erbten den Fallback – jetzt gesetzt (`magazin-detail`, `impressum`, `privacy`, + `terms`, `cookie-policy`). OG-Tags leiten sich automatisch ab. +- Navigation: `Home · Marken · Magazin · Immobilien · Über B2in`. „Netzwerk" ist raus (Seite bleibt + erreichbar), FAQ steht im Footer. „Referenzen" kommt mit AP5 dazu. +- Footer-Claim „Premium-Einrichtung. Persönlich gemacht." (siehe B4). + +**Startseite** – neue Reihenfolge in `resources/views/web/home.blade.php`: +`header → hero → marcel-badge → weiche → kernmarken → über-marcel → sponsoring → immobilien-teaser → abschluss-cta → footer` +- Ankündigungsleiste neu belegt (Rotpunkt Küchen), inkl. neuer `id`. +- Hero: neue Headline, Subline, zwei Buttons, drei Checkmarks, Bild-Badge „Einrichtung, die bleibt." +- Marcel-Badge umgetextet. +- **Neu:** `Sections/AudienceSwitch` (Weiche Privat/Geschäft) – ersetzt „Zwei Welten. Ein Netzwerk." +- **Neu:** `Sections/CoreBrands` mit CABINET, Rotpunkt Küchen, Febrü (Anker `#marken`). + Febrü bewusst mit Home-Office zuerst. **Datenquelle ist vorerst das CMS-Sektionsfeld – + in AP2 wird sie durch das Model `CmsBrand` samt Detailseiten ersetzt.** Das Markup bleibt. +- **Neu:** `Sections/Sponsoring` (AP6) und `Sections/ImmobilienTeaser`. +- „Ein Ökosystem, drei Säulen" ist von der Startseite genommen, die Komponente bleibt als Archiv. + +**Sponsoring-Logos:** je Verein eine Datei gewählt, auf max. 400 px Breite herunterskaliert +(326 KB → 130 KB) und nach `public/img/assets/sponsoring/` übernommen. Die drei Seitenverhältnisse +sind sehr unterschiedlich (500×262, 689×689, 2443×741), deshalb eine feste Box je Logo. +`tc-bad-waldliesborn-horn` liegt nur als JPG vor und hat damit keinen transparenten Hintergrund. + +**Durchgang Alt-Begriffe:** auf allen Live-Seiten bereinigt – Startseite, `/about`, `/contact`, +`/immobilien` sind sauber; `/netzwerk` und `/faq` ebenfalls. Nicht angefasst: die +**Magazin-Artikel**, die inhaltlich über Supply-Chain und Investments handeln – das sind +redaktionelle Beiträge, keine Seitentexte. + +**Tests:** `tests/Feature/HomePageStructureTest.php` (11 Fälle: Reihenfolge, Hero-Buttons und +-Checkmarks, Kernmarken samt Anker, Febrü-Gewichtung, Navigation, SEO, Sponsoring-Logos); +`AnnouncementBarTest` und `LiveSiteReviewTest` auf die neuen Inhalte gezogen. + +--- + +#### ⚠️ Wichtig fürs Deployment: der Relaunch-Seeder + +`cms_theme_section()` bevorzugt das CMS gegenüber der Lang-Datei. 14 der neu getexteten Sektionen +lagen bereits mit den **alten** Texten im CMS und hätten die neuen Inhalte überdeckt – der reguläre +`CmsContentSeeder` fasst bestehende Einträge seit B1 bewusst nicht mehr an. + +Deshalb gibt es `InteriorRelaunchCmsSeeder`: er überschreibt **genau die 19 in AP1 geänderten +Sektionen** aus den Lang-Dateien und lässt alles andere in Ruhe. + +``` +php artisan db:seed --class=InteriorRelaunchCmsSeeder +``` + +Auf der lokalen Datenbank bereits gelaufen (5 angelegt, 14 aktualisiert). **Auf dem Live-System +vor dem Lauf ein DB-Backup ziehen** – er überschreibt in diesen Sektionen auch redaktionelle +Änderungen. Danach `npm run build:web`. + +--- + +#### Nebenbefund B12 · `/service` und `/portfolio` lieferten 500 ✅ + +Beide Seiten sind öffentlich erreichbar, aber ihre Content-Sektionen (`hero_image`, +`content_section_left/right`, `portfolio`) existieren in keiner Lang-Datei mehr – vermutlich seit +dem Februar-Relaunch. Sie warfen seitdem einen 500er; da sie in keinem Test vorkamen, ist es +niemandem aufgefallen. **Verifiziert: älter als AP1, keine Regression.** + +**Behoben** wie bei den anderen stillgelegten Seiten per 301 (`/service` → `/netzwerk`, +`/portfolio` → `/immobilien`); die Views bleiben als Archiv liegen. Beide Ziele sind eine +Zeile in `routes/web.php`, falls Du woandershin umleiten möchtest. + +--- + +#### Was in AP1 offen bleibt + +| Punkt | Grund | +|---|---| +| Hero-Bildmotiv, Kachelbilder, OG-Image | Zulieferung offen, Bestandsbild bleibt | +| Navigationspunkt „Referenzen" | kommt mit der Seite in AP5 | +| Teaser „Aktuelles aus unseren Showrooms" | kommt mit AP5, sonst zeigt er ins Leere | + +--- + +#### Logo mit Zusatz „Interior" ✅ (Zulieferung eingetroffen) + +Marcel hat die Dateien geliefert (`dev/2026-08-28/logos/`) – die typografische Eigenlösung +entfällt damit. Beide Varianten sind identisch aufgebaut und unterscheiden sich nur im Füllwert: +positiv `#314052`, negativ `#FFFFFF`; der blaue Akzent `#2C9FDA` bleibt in beiden. Trotz „CMYK" +im Dateinamen enthalten die SVGs RGB-Hexwerte, die Schrift liegt in Pfaden. + +**Zuschnitt war nötig:** Die Lieferung lag auf einem A4-Artboard (595,3 × 419,5) – gemessen +**74 % leerer Weißraum**. Ungeschnitten hätte das Logo bei gleicher CSS-Höhe rund halb so groß +gerendert wie das bisherige. Der Inhaltsrahmen wurde durch Rendern ermittelt und das viewBox auf +`153 96 289.3 227.5` gezogen (98 % Füllgrad, gleichmäßige Randluft, Verhältnis 1,27:1). + +Abgelegt als `public/img/logos/b2in-interior-logo-{positive,negative}.{svg,png}`; die alten +`b2in-logo-*`-Dateien bleiben als Archiv liegen. + +**Angepasst:** `ThemeHelper` (b2in → neue Dateien) · Header `h-10` → `h-12` und Footer `h-14` → +`h-16`, weil das neue Lockup gestapelt ist und die Wortmarke bei gleicher Höhe kleiner wirkt · +Footer- und Mail-`width`/`height`-Attribute auf das neue Verhältnis korrigiert (standen auf +2,14:1 bzw. 3,5:1 und passten schon vorher nicht) · Mail-Header nutzt das neue PNG. + +**Display ebenfalls umgestellt** (Marcel möchte es laut Mail einheitlich): + +- Beide Varianten liegen jetzt auch unter `public/_cabinet/assets/`. +- `DisplayModuleSettings`: Default-Logo und Claim getauscht. Der Claim stand dort noch auf + „Connecting Design & Property". +- **Variante nach Theme:** Das Standard-Theme ist `dark`, dort liegt das Logo auf einem dunklen + Scrim – bislang stand da die **dunkle** Variante (deshalb der `drop-shadow` im Player). Jetzt + dunkles Theme → negativ/weiß, helles Theme → positiv. +- Logo-Höhe im Player von `3.5vh` auf `4.6vh`, weil das neue Lockup gestapelt ist. +- `DisplayInteriorBrandingSeeder` vereinheitlicht die bestehenden Module. Vorher liefen dort + **vier verschiedene Logo-Quellen** nebeneinander (zwei Dateien aus der Medienbibliothek, ein + relativer und ein absoluter Asset-Pfad) und zwei Module trugen den Claim „Hello". + +--- + +#### B13 · Live-Displays zeigten auf eine lokale Entwicklungsdomain ⛔→✅ + +Beim Vereinheitlichen aufgefallen: Medien-URLs lagen als **absolute URLs mit fest verdrahtetem +Host** in den Moduleinstellungen und Item-Inhalten – teils `b2in.eu`, teils **`b2in.test`**. +Der Player gibt absolute URLs unverändert zurück, ein auf `b2in.test` verweisendes Medium kann auf +dem Live-Display also nicht laden. + +**Betroffen waren vier veröffentlichte Playlists:** „Display 4 – Innen", „Kundenstopper", +„Display 2" und „Test-Display" – unter anderem über das Modul „Cabinet-Influencerin", das auf +drei der vier Displays läuft. + +**Behoben:** `DisplayMediaUrlNormalizerSeeder` schreibt Schema + Host auf einen reinen Pfad um. +Relative Pfade löst der Player über `resolveDisplayUrl()` gegen die Portal-Basis auf und +funktionieren damit auf Test- **und** Live-System. 23 Verweise umgeschrieben, 0 verblieben. + +Ziel-Links (Footer-URL, QR-Ziel wie `cabinet-bielefeld.de`) bleiben bewusst absolut – der Filter +greift nur bei einem B2in-Host **und** einem Pfad unterhalb von `/storage/`. Nachgeprüft: die +fünf Ziel-Links stehen unverändert. + +**Tests:** `DisplayInteriorBrandingTest` (6) und `DisplayMediaUrlNormalizerTest` (6). + +⚠️ **Wichtig:** Ob die betroffenen Mediendateien auf dem Live-Server überhaupt vorhanden sind, +kann ich von hier nicht prüfen. Wurden sie nur lokal hochgeladen, fehlen sie dort weiterhin – +der relative Pfad ist dann trotzdem richtig, aber die Datei muss nachgezogen werden. +Nach dem Deployment bitte die vier Displays einmal sichten. + +**Deployment-Reihenfolge:** +``` +php artisan db:seed --class=InteriorRelaunchCmsSeeder +php artisan db:seed --class=DisplayInteriorBrandingSeeder +php artisan db:seed --class=DisplayMediaUrlNormalizerSeeder +npm run build:web +``` + +### AP2 · Kernmarken-Modul im CMS — ✅ umgesetzt + +**Model + Migration:** `CmsBrand` / Tabelle `cms_brands`. +⚠️ Bewusst **nicht** `Brand` – das Model existiert bereits im Produkt-/Partner-Kontext +(`Partner::brand()`, `Product::brand()`) und bleibt unangetastet. Ein Test hält das fest. + +Felder: `slug`, `name`, `tagline`, `teaser`, `description` (HTML), `logo`, `image`, `services`, +`gallery`, `website_url`, `showroom`, `is_published`, `order`. +Übersetzt werden `name`, `tagline`, `teaser`, `description`. + +**Die Lehre aus B2 ist eingebaut:** Das Frontend liest direkt aus dem Model, nicht aus einer +Lang-Datei. `CmsBrand::forFrontend()` liefert die veröffentlichten Marken nach Slug indiziert; +solange keine Marke gepflegt ist, greift die Lang-Sektion `core_brands` als Fallback. Die +Fallback-Einträge tragen `has_detail_page = false`, damit die Kachel dann nicht auf eine +Detailseite verlinkt, die es noch nicht gibt. + +**Admin:** `admin/flux-cms/brands` → `brands-index.blade.php` (Volt), aufgebaut nach dem +Magazin-Muster. Medien über den bestehenden `MediaPicker` (Logo, Stimmungsbild, Galerie), die +Einleitung über den Flux-Editor mit `CmsFluxEditorHtmlTransformer`. Slug wird auf Format und +Eindeutigkeit geprüft. Eigene Karte im CMS-Dashboard. + +**Frontend:** +- `Sections/CoreBrands` rendert die Kacheln aus dem Model (Anker `#marken`). +- Neue Route `/marken/{slug}` mit `Sections/BrandDetail` und der Vorlage-Struktur aus + `umsetzung-inhalte.md` §5: Header (Logo + Claim + Stimmungsbild) → Einleitung → Leistungen → + Galerie → CTA, dazu eine Leiste mit den übrigen Marken. +- Eigener Title und eigene Description je Marke. +- Unveröffentlichte Marken liefern 404 und verschwinden von der Startseite. + +**Seeder:** `CmsBrandSeeder` legt CABINET, Rotpunkt Küchen und Febrü mit den Texten aus +`umsetzung-inhalte.md` §5 sowie je vier Leistungen an – Febrü mit Home-Office zuerst. +Bestehende Marken werden **nicht** überschrieben, Marcel pflegt sie im Admin. + +**Tests:** `Cms/CmsBrandTest` (8), `BrandPageTest` (11), `Admin/Cms/CmsBrandsIndexTest` (11) – +darunter die Gegenprobe zu B2: eine im CMS neu angelegte Marke erscheint ohne Codeänderung auf +der Startseite **und** bekommt ihre Detailseite. + +#### Nachtrag: eigene Unterseite `/marken` + +Auf Wunsch nach der Sichtung – Grundlage `dev/2026-08-28/unterseite_marke.md`. Die Startseite +bleibt wie sie ist, die Kernmarken-Sektion dort ist jetzt ein **Teaser**. + +- **Neue Seite `/marken`** mit dem Aufbau aus der Spec: Seitenkopf mit Breadcrumb (Home → Marken) + → Kernmarken → Partnernetzwerk → „Markenwelt wächst" → Abschluss-CTA. Eigener Title und eigene + Description. +- **Zwei Textebenen je Marke.** Dafür kamen zwei Felder dazu (`summary`, `highlights`): + + | Feld | Wo | + |---|---| + | `teaser` | Kachel auf der Startseite (gekürzt) | + | `summary` | Text auf `/marken` (zwei Absätze) | + | `highlights` | die drei Merkmale auf `/marken` | + | `description` | Einleitung der Detailseite | + | `services` | Leistungen der Detailseite | + +- **Startseite:** Kacheln verlinken direkt auf die Detailseite, darunter der Button + „Alle Marken ansehen" → `/marken`. +- **Navigation:** „Marken" zeigt auf `/marken` statt auf den Anker `/#marken`; der Hero-Button + ebenfalls. Der Anker `#marken` bleibt für Direktsprünge erhalten. +- **Slug korrigiert:** Die Spec nennt `/marken/rotpunkt`, geseedet war `rotpunkt-kuechen`. + Angepasst, solange nichts live ist – später wäre das ein Redirect gewesen. +- **`/netzwerk` stillgelegt.** Der Inhalt geht im Abschnitt „Mehr als Marken: unser Netzwerk" auf + `/marken` auf. `/netzwerk`, `/interior`, `/partner` und `/service` leiten jetzt **direkt** auf + `/marken` – vorher liefen `/interior` und `/partner` über `/netzwerk`, also eine Redirect-Kette. + Die View bleibt als Archiv liegen. +- **WhatsApp-Button:** erscheint nur, wenn im CMS unter `global.contact.whatsapp` eine Nummer + hinterlegt ist (`whatsapp_link()`). Solange die Nummer nicht bestätigt ist, zeigt der CTA nur + „Termin anfragen" – kein Button auf eine erfundene Nummer. + +**Tests:** `MarkenPageTest` (9), `InteriorPageTest` neu geschnitten (4), dazu zwei Fälle in +`Admin/Cms/CmsBrandsIndexTest` – darunter die Gegenprobe, dass eine im Admin angelegte Marke +mitsamt Text und Merkmalen auf `/marken` erscheint. + +> **Notiz für spätere Reihenfolge-Tests:** `wire:snapshot` serialisiert die Komponentendaten als +> JSON ins Attribut und escaped Umlaute zu `\uXXXX`. Ein `strpos`-Vergleich auf dem rohen HTML +> misst dann den Snapshot statt des sichtbaren Textes. In `MarkenPageTest` werden die Snapshots +> vorher entfernt. + +**Offen:** Logos, Stimmungsbilder und Galerien fehlen als Zulieferung – Kacheln und Detailseiten +zeigen so lange einen Platzhalter. Der WhatsApp-Button im CTA-Block kommt mit AP3, sobald die +Nummer bestätigt ist. + +--- + +#### Nachtrag 2: Galerie je Marke – realisierte Projekte und Inspirationen getrennt + +Aus Marcels Mail: „Der Bereich Referenzen ist mir wichtig … Entscheidend ist für mich, dass dieser +Bereich langfristig mitwachsen und von mir selbst gepflegt werden kann. Für den Start können wir +ergänzend mit hochwertigen Visualisierungen und KI-unterstützten Darstellungen arbeiten. Dabei +sollte allerdings klar zwischen tatsächlich realisierten Referenzen und Entwürfen beziehungsweise +Inspirationen unterschieden werden." + +Umgesetzt als Galerie **auf der Detailseite jeder Kernmarke**, mit zwei Rubriken. + +- **Datenform.** Das bestehende JSON-Feld `cms_brands.gallery` bekommt je Eintrag + `image`, `category` (`project` | `inspiration`) und `caption`. **Keine Migration nötig.** +- **Sicherheitsnetz gegen die schädlichere Verwechslung:** `CmsBrand::normalizeGallery()` stuft + jeden Eintrag ohne oder mit unbekannter Kategorie als **Inspiration** ein – nie als Projekt. + Ein Entwurf, der versehentlich als Referenz durchgeht, wäre der teurere Fehler. Die + Normalisierung greift beim Laden, beim Speichern und beim Ausspielen. +- **Frontend** (`brand-detail.blade.php`): Abschnitt „Referenzen und Inspirationen" mit Umschalter, + sobald beide Rubriken gefüllt sind; ist nur eine gefüllt, steht sie mit eigener Überschrift da. + Jede Kachel trägt ein sichtbares Label („Realisiertes Projekt" / „Visualisierung"), über den + Inspirationen zusätzlich der Satz „Diese Darstellungen sind Entwürfe und Visualisierungen – + keine ausgeführten Projekte." Lightbox mit Pfeiltasten und Esc, Muster übernommen aus + `immobilien-show.blade.php`. +- **Admin** (`brands-index.blade.php`): zwei getrennte Buttons „+ Projekt" und „+ Inspiration", je + Eintrag ein Auswahlfeld und eine Bildunterschrift, dazu ein Hinweis, was in welche Rubrik gehört. + Die Kategorie wird beim Anlegen entschieden, nicht per stillem Vorgabewert. +- **Abgrenzung zu AP5:** AP5 bleibt die Rubrik-Seite „Referenzen" mit dem zweiten Display. Die + markenbezogene Galerie hier ist die pflegbare Grundlage dafür und ersetzt AP5 nicht. + +**Tests:** 5 Fälle in `BrandPageTest` (keine Galerie → kein Abschnitt; beide Rubriken beschriftet; +kein Umschalter bei nur einer Rubrik; Eintrag ohne Kategorie erscheint als Visualisierung; Hinweis +sichtbar), 5 in `Cms/CmsBrandTest` (Normalisierung, Gruppierung), 4 in +`Admin/Cms/CmsBrandsIndexTest` (Anlegen je Rubrik, Speichern, unbekannte Kategorie, Löschen). + +**Offen:** Bildmaterial. Marcel setzt Projektfotos und Visualisierungen selbst ein. + +--- + +### AP3 · Terminanfrage-Widget + WhatsApp — ✅ umgesetzt + +**Korrektur gegenüber Revision 1:** kein Inline-Abschnitt, sondern ein **schwebendes Element +unten rechts auf allen Seiten** → gehört ins Layout `web-master.blade.php`, nicht in `home.blade.php`. + +- Aufsetzen auf `Acme\ContactForm\Livewire\ContactForm` per `customFields` – Honeypot, + Rate-Limit und `SpamDetector` kommen damit geschenkt. +- Felder: Name*, E-Mail*, Telefon, Anliegen/Marke (Auswahl), Wunschtermin, Nachricht. +- **Markenauswahl speist sich aus `CmsBrand`** → Abhängigkeit zu AP2 (Reihenfolge einhalten), + plus feste Einträge „Projektausstattung" und „Sonstiges". +- WhatsApp-Button daneben, Nummer als CMS-Wert (`global.contact.whatsapp`), Link `https://wa.me/`. +- Cookie-Consent beachten (`packages/acme/CookieConsent`). + +**Tests:** `AppointmentWidgetTest` (17 Fälle: Einbindung auf allen Seiten, Markenauswahl aus +`CmsBrand`, Validierung inkl. Wunschtermin in der Vergangenheit, Übergabe an den +`ContactFormService`, Honeypot, WhatsApp sichtbar/unsichtbar); `ContactFormTest` bleibt grün. + +#### Umsetzungsnotizen + +- **Abweichung vom Plan:** nicht auf `Acme\ContactForm\Livewire\ContactForm` per `customFields` + aufgesetzt. Dessen mitgelieferte View bringt ein eigenes, blaues Layout mit, das nicht zum + Auftritt passt. Stattdessen eine eigene Komponente wie `ContactForm` und `ImmobilienContactForm` + der Seite, die `ContactFormService` und `SpamDetector` aus dem Paket nutzt – Honeypot, + Rate-Limit und Spam-Erkennung sind damit trotzdem drin. +- **Platzierung:** `web-master.blade.php`, unten rechts, `z-40`. Der Cookie-Button des + Consent-Managers sitzt unten links (ebenfalls `z-40`), sein Dialog auf `z-50` – die Einwilligung + bleibt also immer erreichbar und wird nie verdeckt. +- **Kein eigener Consent nötig:** Das Widget setzt nichts im Browser, der WhatsApp-Link ist ein + ausgehender Link ohne Einbettung. +- Die `[x-cloak]`-Regel liegt jetzt einmal zentral im Layout – die Seiten brachten sie bisher nur + teilweise selbst mit, das Widget braucht sie überall. +- Felder: Name*, E-Mail*, Telefon, Anliegen (Auswahl), Wunschtermin, Nachricht, Einwilligung*. + Der Wunschtermin ist auf heute oder später begrenzt, im Browser über `min` und serverseitig + über `after_or_equal:today`. +- Betreff der Mail: `Terminanfrage B2in Interior: `, Log-Kontext `appointment-widget` – + so lassen sich Widget-Anfragen von den übrigen Formularen unterscheiden. Die Seite, von der die + Anfrage kam, steht als `source` in der Mail. +- **WhatsApp** erscheint weiterhin nur, wenn unter `global.contact.whatsapp` eine Nummer im CMS + steht. Ohne Nummer zeigt das Widget allein den Termin-Button. + +#### WhatsApp: was technisch geht und was nicht + +Eine echte WhatsApp-Anbindung (Nachrichten empfangen, im CMS beantworten, Verlauf speichern) +setzt die WhatsApp Business Platform voraus: Meta-Verifizierung des Unternehmens, ein Anbieter +für den API-Zugang, laufende Kosten je Konversation. Das ist nicht Teil dieses Auftrags. + +Umgesetzt ist die dafür übliche Alternative, ein **Klick-zum-Chat-Link** (`wa.me`): + +- Der Button sitzt im Terminanfrage-Panel unter dem Absenden-Button. +- Auf dem Handy öffnet die installierte WhatsApp-App, am Rechner WhatsApp Web bzw. die + Desktop-App. Ist WhatsApp nirgends vorhanden, landet man auf der Hinweisseite von `wa.me`. +- Der Nachrichtentext ist **vorbereitet** und wird im Browser aus dem zusammengesetzt, was im + Formular schon steht (Anliegen, Wunschtermin, Name). Der Einstiegssatz ist im CMS pflegbar + (`global.appointment_widget.whatsapp_text`). +- **Abgeschickt wird die Nachricht vom Besucher selbst** – das steht auch so unter dem Button. + +Was das bedeutet, bewusst festgehalten: + +| | | +|---|---| +| Wo landet die Nachricht | in Marcels WhatsApp, nicht im CMS und nicht per Mail | +| Was sehen wir davon | nichts – kein Eintrag, keine Statistik, kein Spamschutz | +| Voraussetzung | eine Mobilnummer, die WhatsApp empfangen kann (sinnvoll: WhatsApp Business) | +| Datenschutz | ausgehender Link zu Meta. In die Datenschutzerklärung gehört ein Absatz dazu; ein Consent-Banner braucht es nicht, weil nichts eingebettet wird und nichts gesetzt wird | + +Wer eine Anfrage im System dokumentiert haben will, nutzt weiterhin das Formular – der +WhatsApp-Weg ist die schnelle, aber unprotokollierte Abkürzung. + +### Nachtrag 3 · Bestandsinhalte auf den Interior-Auftritt umgestellt ✅ + +Nach der Sichtung: Drei verlinkte Seiten erzählten noch die Dubai-/Plattform-Geschichte. +`/immobilien` bleibt bewusst unverändert – die Seite ist der „Außerdem"-Bereich. + +**`/faq`** – die beiden Rubriken „Fokus: Immobilien-Investoren (B2C)" und „Fokus: B2B-Partner" +sind ersetzt durch **„Für Ihr Zuhause"** und **„Für Projekte und Objekte"** (je vier Fragen: +Einstieg mit einem einzelnen Möbelstück, Kosten der Maßanfertigung, Montage, Showroom – +bzw. Projektgröße, Termintreue, Architekten, Netzwerk). Von den allgemeinen Fragen waren zwei +noch Dubai-lastig, die sind jetzt Beratungsablauf und Verlässlichkeit. Die englische Fassung war +noch komplett auf dem alten Stand und ist mitgezogen. + +**`/about`** – das Mission-Zitat sprach von „digitalen Werkzeugen" und „Brücken zwischen Märkten". +Neu: wenige ausgewählte Hersteller, Fachbetriebe aus der Region, ein Ansprechpartner. Die sechs +Werte waren Plattform-Vokabular (Innovation, Konnektivität, Design-Exzellenz) und heißen jetzt +Beratung statt Verkauf, Maßarbeit, Ausgewählte Marken, Regional verwurzelt, Ein Ansprechpartner, +Verlässlichkeit. In der Zeitleiste sind „Die Idee", „Das Fundament" und „Die Vision" umgeschrieben. + +**`/contact`** – die Betreffliste beginnt jetzt mit Beratungstermin, Maßanfertigung und +Küchenplanung; „Internationale Immobilien" rutscht nach hinten, bleibt aber erhalten. + +**`/magazin`** – zwei neue Beiträge, die **an erster Stelle** stehen: +`massanfertigung-warum-der-millimeter-zaehlt` und `kueche-planen-vom-wunsch-zum-grundriss`. +Die vier Dubai-Beiträge bleiben, sie gehören zum Immobilienbereich. + +> ⚠️ Die beiden Beiträge sind **Platzhalter**. Bewusst ohne Referenzen, Zahlen oder Namen +> geschrieben, damit nichts behauptet wird, was nicht belegt ist – vor dem Livegang von Marcel +> gegenlesen lassen. Als Bilder dienen vorhandene Motive aus dem Altbestand. + +**Zwei technische Punkte dabei:** + +- Neue Lang-Artikel bringen ihren Slug jetzt selbst mit (`'slug' => …`, aufgelöst über + `CmsArticle::slugForLangArticle()`). `LEGACY_ID_TO_SLUG` bleibt damit das, was der Name sagt: + die Karte für die alten `/magazin/{id}`-URLs. `/magazin/6` ist korrekt ein 404, weil es diese + URL nie gab. +- `CmsArticleSeeder` sortiert nicht mehr per `ksort`. Die Reihenfolge in der Lang-Datei bestimmt + jetzt die Reihenfolge im Magazin – im CMS über `order`, im Lang-Fallback über die + Array-Reihenfolge. Vorher konnten beide Wege auseinanderlaufen. + +**Tests:** vier Fälle in `MagazinPageTest` (Reihenfolge, beide Slugs, kein erfundener Legacy-Link, +Slug-Auflösung), drei in `LiveSiteReviewTest` (FAQ ohne Dubai mit neuen Rubriken, About mit den +neuen Werten, Kontakt mit den neuen Betreffen). + +**Nachkontrolle:** Gegen die Live-Seiten geprüft, nicht nur gegen die Sprachdatei – dabei fiel in +der FAQ eine dritte Frage auf, die noch vom „Local for Local"-Netzwerk sprach. Ebenfalls korrigiert. + +**Noch offen auf diesen Seiten:** eigene Beitragsbilder für die zwei neuen Artikel; die vier +Dubai-Beiträge bleiben inhaltlich unverändert stehen. + +--- + +### Nachtrag 4 · Markenseiten Rotpunkt und Febrü aus der Zulieferung ✅ + +**Grundlage:** zunächst `Rotpunkt_Kuechen_Webseitentext_B2in_FINAL.pdf` und +`Februe_Webseitentext_B2in_FINAL.pdf` (Stand 31.08.2026, im selben Ordner). Beide enthalten neben +dem Seitentext einen Abschnitt „UMSETZUNGSHINWEIS FÜR KEVIN – WEBSTRUKTUR UND CTA“. + +> Die als Aufgabenliste angekündigte Datei `umsetzung-03-09-2026.md` war zu diesem Zeitpunkt leer +> (0 Byte). Sie wurde nachgereicht – **die Auswertung steht in Nachtrag 5**, der auch die +> korrigierten Textfassungen (`*_FINAL.md`) einarbeitet. Für den Textstand gilt Nachtrag 5. + +**Was die Zulieferung verlangt:** + +1. Der normale Seitentext bleibt direkt sichtbar. +2. Die mit „AUFKLAPPBARER DETAILBEREICH“ gekennzeichneten Vertiefungen werden aufklappbar. +3. **Kein zweiter CTA-Button.** Stattdessen bekommt das vorhandene schwebende + Terminanfrage-Widget auf der jeweiligen Markenseite eine kontextbezogene Beschriftung. +4. Das Widget bleibt beim Scrollen sichtbar und darf auf dem Smartphone nichts verdecken. +5. Die internen Quellenhinweise am Ende der PDFs werden **nicht** veröffentlicht. + +#### Umsetzung + +**Datenstruktur.** `description` (Einleitung) und `services` (Kacheln) reichten für diese Texte +nicht aus. Neu in `cms_brands`: + +| Feld | Inhalt | +|---|---| +| `sections` | Abschnitte der Seite: Titel, Absätze, optional Schlusszeile, Zitat und Vertiefung | +| `widget_label` | Beschriftung des Terminanfrage-Widgets auf dieser Markenseite | +| `cta_title` / `cta_text` | Abschlussblock der Seite | + +`sections` ist – wie `services`, `highlights` und `gallery` – bewusst **nicht** übersetzbar; die +Texte liegen nur auf Deutsch vor. Die drei Textfelder folgen den übrigen Textfeldern und sind +übersetzbar. `CmsBrand::normalizeSections()` bringt jeden Eintrag in eine feste Form und nimmt +Absätze wahlweise als Liste oder als Textblock entgegen (Leerzeile trennt) – dadurch lässt sich im +Admin einfach in ein Textfeld schreiben. + +**Darstellung.** Die Abschnitte stehen zwischen Einleitung und Leistungen. Vertiefungen sind +aufklappbar, mit `aria-expanded` und `aria-controls`. Bewusst `x-transition` statt `x-collapse`: +Das Collapse-Plugin ist in diesem Projekt nicht registriert, die Bewegung entspricht jetzt dem +FAQ-Akkordeon. Zitate stehen abgesetzt mit Nennung des Sprechers, Schlusszeilen hervorgehoben. + +**Widget-Beschriftung.** Das Widget steckt im Layout, nicht in der Seite – es liest den Kontext +deshalb aus der Route: Auf `marken.show` greift `widget_label` der Marke, sonst die allgemeine +Beschriftung. Ein Test prüft gegen, dass dabei **kein zweiter Button** entsteht. + +**Platz auf dem Smartphone.** Der Footer bekommt unten zusätzlichen Abstand (`pb-28 sm:pb-16`), +und der Button bricht lange Beschriftungen um, statt über den Bildschirmrand zu laufen – „Persönliche +Arbeitsplatzberatung vereinbaren“ ist deutlich länger als das vorherige „Termin anfragen“. + +**Pflegbarkeit.** Die Abschnitte sind vollständig im Admin pflegbar: hinzufügen, löschen, per Pfeil +sortieren, je Abschnitt Überschrift, Text, Schlusszeile, Zitat und Vertiefung. Dazu die drei neuen +Felder für Abschluss und Widget-Beschriftung. + +**Einspielen.** `BrandPageTextSeeder` schreibt die Texte. Er fasst **nur** `tagline`, +`description`, `sections`, `widget_label`, `cta_title` und `cta_text` an – Bilder, Galerie, +Leistungen und Reihenfolge bleiben unangetastet. Ein Test prüft genau das. + +**Nebenbei korrigiert:** Febrüs Claim hieß „Arbeiten, wo man gerne ist.“, die Zulieferung schreibt +„Arbeiten, wo man gerne **bleibt**.“ + +**Tests:** `BrandPageSectionsTest` (17 Fälle: Abschnitte beider Marken, Anzahl der aufklappbaren +Bereiche, Zitat, Schlusszeilen, keine Quellenhinweise im Ausgabe-HTML, markenspezifischer +Abschlussblock mit Rückfall auf den allgemeinen, Widget-Beschriftung je Marke, kein zweiter Button, +Normalisierung, Redaktionsschutz des Seeders) und vier neue Fälle in `Admin/Cms/CmsBrandsIndexTest`. + +> ⚠️ **Vor der Veröffentlichung freigeben lassen.** Die PDFs halten fest: Aussagen nur im belegten +> Umfang veröffentlichen, Herstellerfreigabe einholen, und die Rotpunkt-Aussage zur jährlich +> erhaltenen **Waldfläche von ungefähr 50 Fußballfeldern** nur mit ausdrücklicher Bestätigung +> verwenden. Sie steht als eigener Absatz in der Vertiefung „Zahlen, Zertifizierungen und +> Raumklima“ und lässt sich dort einzeln entfernen. Als Punkt in Abschnitt 8 aufgenommen. + +**Offen:** CABINET hat noch keinen ausführlichen Seitentext – die Seite läuft weiter über +Einleitung und Leistungskacheln. Bilder fehlen weiterhin für alle drei Marken. + +--- + +### AP4 · Display-Spiegelung auf der Website — ✅ umgesetzt + +**Korrektur gegenüber Revision 1:** Revision 1 empfahl, die Slides nativ zu rendern. Bei 3 Std. +ist das nicht zu halten, und die Umsetzungsliste schreibt „nutzt die vorhandene Display-/Player-API +als Datenquelle". → **Einbettung des vorhandenen Players im 9:16-Container.** + +- Sektion `Sections/DisplayFeed` mit `aspect-ratio: 9/16`, darin der Player. +- **Vorher zu klären:** Die `/preview/…`-Routen sind token- bzw. modulbasiert und für den internen + Gebrauch gedacht. Für die öffentliche Domain braucht es eine schmale, read-only Route + (z. B. `/embed/display/{display}`), die keine Preview-Tokens nach außen gibt. +- Keine doppelte Pflege: Marcel pflegt weiterhin nur im Display-CMS. + +#### Umsetzung + +- **Route `GET /embed/display/{display}`** (`DisplayEmbedController`), bewusst getrennt von den + Preview-Routen: Die arbeiten mit `preview_token` und zeigen den **Entwurf**. Nach außen darf + weder ein Token noch ein unveröffentlichter Stand gelangen. Die Embed-Route kennt nur die + numerische ID, liefert 404 bei inaktivem Display, bei reinem Entwurf und bei leerer Playlist, + und setzt `X-Robots-Tag: noindex`. +- **Kein Proxy, keine CORS-Frage:** Der Player liest die ID aus dem Pfad (`/display/{id}` steckt in + `/embed/display/{id}`) und holt seine Konfiguration über die bereits öffentliche Route + `/api/display/{display}/config` – dieselbe Quelle wie das physische Display. Da die API-Routen + für alle Domains registriert sind, läuft der Abruf gleichursprünglich über `b2in.test`. + Möglich wurde das durch **B13**: Vorher standen absolute `b2in.test`-URLs in den Playlists. +- **Sektion `Sections/DisplayFeed`** im 9:16-Rahmen. Welches Display gezeigt wird, steht im CMS + (`referenzen.display_feed.display_id`); ohne Eintrag greift das erste aktive Display mit + veröffentlichter Playlist. Gibt es keins, rendert die Sektion **nichts** – lieber keine Sektion + als ein Rahmen mit ewigem Ladebalken. Auch die Texte bleiben dann leer, sonst stünden sie + im Livewire-Snapshot der Seite. +- Eingebunden auf der Startseite (an der dafür vorgesehenen Stelle) und auf `/referenzen`. + +**Tests:** `DisplayWebFeedTest` (10 Fälle: Embed liefert die Datei, kein Token im Ausgabe-Inhalt, +404 bei inaktiv/Entwurf/leer/unbekannt, Einbindung auf der Startseite, CMS-Auswahl und Fallback). + +### AP5 · Rubrik-Seite „Referenzen" — ✅ umgesetzt (Pflege seit Nachtrag 13 als eigenes Modul) + +**Korrektur gegenüber Revision 1:** Revision 1 schlug einen neuen `DisplayVersionType`-Case vor. +Die Umsetzungsliste sagt „zweites, separates **Display** im Display-CMS" – das ist ein weiterer +`Display`-Datensatz und braucht **keine Enum-Änderung**. Damit entfallen die Folgeänderungen an +`DisplayPlaylistConfigBuilder::moduleData()`, `DisplayModuleSettings` und den +`version-editor-*`-Partials. Deutlich risikoärmer und billiger. + +- Zweites `Display` anlegen, Route `/referenzen`, View + Sektion, Aufnahme in die Navigation. +- Startseiten-Teaser „Aktuelles aus unseren Showrooms" mit Button „Alle Referenzen ansehen →". +- 2–3 Platzhalter-Einträge. +- YouTube-Verlinkungen mit Consent-Gate. +- Inhaltliche Regel dokumentieren: unter „Referenzen" nur realisierte Projekte; Visualisierungen + und Entwürfe gehören in die Kernmarken-Detailseiten. +- **Bereits vorhanden:** die markenbezogene Galerie mit den Rubriken „Realisierte Projekte" und + „Inspirationen und Entwürfe" (siehe Nachtrag 2 zu AP2). AP5 baut darauf auf, statt eine zweite + Pflegestelle für dieselben Bilder zu schaffen. + +#### Umsetzung + +- Seite `/referenzen`: Seitenkopf → realisierte Projekte → Showroom-Display → Abschluss-CTA. + Navigationspunkt zwischen „Marken" und „Magazin". +- **Kein zweites Display nötig, keine zweite Pflegestelle:** Die Projekte kommen aus den + Markengalerien (AP2) und werden über alle Marken hinweg gesammelt. Es erscheinen ausschließlich + Einträge der Rubrik **„Realisiertes Projekt"** – Visualisierungen bleiben draußen, damit + „Referenzen" hält, was das Wort verspricht. Jede Kachel verlinkt zurück auf ihre Markenseite. +- **Ehrlicher Leerzustand:** Solange keine Projekte gepflegt sind, steht dort ein Satz dazu und ein + Terminlink – keine erfundene Referenz und keine leere Fläche. +- Der Seitenkopf ist jetzt eine wiederverwendbare Sektion (`PageHero` mit `section` und + `breadcrumb`), vorher war er auf `/marken` festgelegt. + +**Tests:** `ReferenzenPageTest` (6 Fälle: Seite lädt mit eigenem Titel, Navigationspunkt, +Projekte aus allen Marken, Visualisierungen bleiben draußen, Rückverlinkung, Leerzustand). + +### AP6 · Engagement / Sponsoring — ✅ umgesetzt (im Rahmen von AP1) + +Schlanke Sektion „Regional verbunden" nach der Marcel-Sektion: ein Einleitungssatz plus drei +Logos in ruhiger Reihe (Graustufen/gedeckt). + +Logos liegen bereits unter `dev/2026-08-28/sponsoren-logos/` – **7 Dateien für 3 Vereine** +(Arminia, TCBWH, TCH in mehreren Varianten und Formaten). Vor der Umsetzung je Verein **eine** +Datei auswählen, nach `public/img/assets/` bzw. in die Medienbibliothek übernehmen und +vereinheitlichen (gleiche Höhe, transparenter Hintergrund – die `.jpg`-Varianten haben keinen). +Setzt **B9** voraus, damit die Logos per Picker pflegbar sind. + +--- + +--- + +### Nachtrag 5 · Abstimmungsrunde 03.09.2026 ✅ + +**Grundlage:** `dev/2026-08-28/umsetzung-03-09-2026.md`. Die Datei war beim ersten Zugriff leer +und wurde nachgereicht. Dazu kamen die korrigierten Textfassungen als Markdown: +`Rotpunkt_Webseitentext_FINAL.md` und `Februe_Webseitentext_FINAL.md`. **Sie lösen die PDFs vom +31.08. ab** – die PDFs bleiben nur als Beleg im Ordner liegen. + +Vier Punkte, alle umgesetzt: + +| # | Punkt | Stand | +|---|---|---| +| 1 | Markenseiten umfangreicher: Rotpunkt und Febrü korrigiert, CABINET neu | ✅ | +| 2 | „BEISPIEL"-Kennzeichnung bei Visualisierungen entfernen | ✅ | +| 3 | Hinweis zu Visualisierungen eleganter formulieren | ✅ | +| 4 | Abschluss-CTA hochwertiger | ✅ | + +#### 1a · Korrigierte Fassungen Rotpunkt und Febrü + +Die beiden Markdown-Dateien führen ihre Änderungen gegenüber dem 31.08. selbst auf. Übernommen: + +- **Rotpunkt:** „Nachhaltigkeit mit konkreter Substanz" und „Greenline hält dem Vergleich stand" + sind **zusammengeführt**; der doppelte Satz „Nachhaltigkeit braucht keine großen Versprechen…" + steht nur noch einmal. Rotpunkt hat damit **8 statt 9 Abschnitte**, weiterhin 4 Vertiefungen. +- **Febrü:** Der erklärende Satz zum Blauen Engel („Die Aussage bezieht sich ausdrücklich auf die + offiziell gelisteten Produktgruppen…") ist aus dem Kundentext **entfernt** – der Produktbezug + steckt jetzt im Satz selbst („Ausgewählte Febrü-Tischsysteme und Stauraummöbel"). Ein Test + hält diese Formulierung fest, damit sie beim Redigieren nicht pauschal wird. +- Die Schlusszeile „Nachhaltig ist nicht das Möbelstück mit dem längsten Werbetext…" steht laut + korrigierter Fassung **innerhalb** der Vertiefung, nicht als Abschnitts-Claim. + +Ein Prüfskript hat jeden veröffentlichten Satz des Seeders gegen die Markdown-Datei gehalten: +0 Abweichungen, alle Überschriften der Vorlage vorhanden. Einzige bewusste Abweichung sind +typografische Anführungszeichen („…“ statt „…"). + +#### 1b · CABINET nach demselben Muster + +Für CABINET **lag keine Zulieferung vor**. Die Seite ist nach dem Vorbild der beiden abgestimmten +Texte von uns verfasst: Einleitung mit Claim, 8 Abschnitte, 3 davon mit Vertiefung, eigener +Abschlussblock „Ihr Raum. Persönlich geplant." und die Widget-Beschriftung „Persönliche +Schrankplanung vereinbaren". + +> ⚠️ **Bewusste Zurückhaltung:** Der Text beschreibt ausschließlich die eigene Arbeitsweise – +> Aufmaß, Planung, Innenausstattung, Fronten, Montage, Schnittstelle zur Tischlerei. Er enthält +> **keine Zahlen, Zertifizierungen oder Herstelleraussagen**, weil es dafür keine Quelle gibt. +> Die Rotpunkt- und Febrü-Seiten stützen solche Aussagen auf die Quellenhinweise der Zulieferung; +> für CABINET wäre das erfunden. **Der Text braucht Marcels Freigabe** (Punkt in Abschnitt 8). + +**Nebenbei korrigiert:** Febrüs Claim stand in `CmsBrandSeeder` und `b2in.php` weiter als +„Arbeiten, wo man gerne ist." – die Zulieferung schreibt „…gerne **bleibt**." + +#### 2 · „BEISPIEL"-Kennzeichnung entfernt + +Die Demobilder trugen den Vermerk „BEISPIEL – " in der Bildunterschrift, damit ein Stockmotiv +nicht versehentlich als echte Referenz durchgeht. Der Vermerk ist entfallen. + +Zwei Stellen waren nötig, weil die Bilder auf Vorschau und Live **bereits in der Datenbank +stehen**: + +- `DemoBrandGallerySeeder` – Bildunterschriften ohne Vermerk (für frische Installationen). +- `BrandGalleryCaptionNormalizerSeeder` **(neu)** – entfernt den Vermerk in vorhandenen Daten. + Fasst **ausschließlich den Text** an; Bild, Kategorie und Reihenfolge bleiben. Mehrfaches + Ausführen ist unbedenklich. + +> ⚠️ Die Unterscheidung hängt damit allein an der **Kategorie** (Badge „Realisiertes Projekt" / +> „Visualisierung"). Die Demobilder der Kategorie `project` sind Stockmotive und KI-Renderings – +> sie behaupten jetzt ohne den Vermerk, echte Referenzen zu sein. **Vor dem Livegang ersetzen +> oder auf „Inspiration" umstellen** (Punkt in Abschnitt 8). + +#### 3 · Hinweis zu Visualisierungen + +| Wo | Vorher | Jetzt | +|---|---|---| +| Markengalerie, Rubrik Inspirationen | „Diese Darstellungen sind Entwürfe und Visualisierungen – keine ausgeführten Projekte." | „Entwürfe und Visualisierungen. Sie zeigen gestalterische Möglichkeiten, noch bevor das erste Möbel steht." | +| Markengalerie, Einleitung | „…und Visualisierungen, die zeigen, was möglich ist." | „…und Visualisierungen, die zeigen, was in einem Raum möglich ist." | +| `/referenzen`, Seitenkopf | „…keine Entwürfe und keine Visualisierungen." | „Hier zeigen wir ausschließlich Arbeiten, die wir tatsächlich ausgeführt haben." | +| `/referenzen`, Projektliste | „…finden Sie bewusst getrennt davon auf den Markenseiten." | „…zeigen wir bewusst getrennt davon – auf den Markenseiten." | + +Die Trennung bleibt inhaltlich unverändert, sie wird nur nicht mehr über eine Verneinung +transportiert. Der Badge am Bild („Visualisierung") trägt die Unterscheidung ohnehin. + +#### 4 · Abschluss-CTA + +Der Block war eine einfarbige Fläche mit Überschrift, Text, Button und einem unterstrichenen +Textlink. Neu: + +- Eine schmale Linie über der Überschrift setzt den Abschluss ab. +- Hauptbutton weiß auf Markenfarbe, mit Pfeil, der beim Hovern mitgeht. +- Die Herstellerseite ist jetzt ein gleichwertiger Outline-Button statt eines Textlinks. +- Der Showroom steht als kleine Zeile mit Pin-Symbol darunter. + +**Nachgebessert (dritte Rückmeldung):** Nach dem Build stand der Abschluss hellgrau mit weißer +Schrift da. Ursache: `.variante-glass-flow section` in `shared-styles.css` färbt **jede** Sektion +in diesem Wrapper hell ein und ist mit `(0,1,1)` spezifischer als `.cta-band` mit `(0,1,0)`. Für +`.bg-secondary` und `.bg-accent` gibt es dort längst Ausnahmen – die neue Klasse brauchte +dieselbe. `/marken/{slug}` hängt in genau diesem Wrapper, deshalb fiel es erst dort auf. Der +Verlauf steht jetzt in `--cta-band-gradient` und wird von beiden Regeln benutzt, damit er nicht +doppelt gepflegt werden muss. Ein Test hält die Ausnahme fest. + +**Nachgebessert (zweite Rückmeldung):** Der erste Versuch legte zwei weiche Kreise (`blur-3xl`) +über das Hellblau – die wirkten als sichtbare Flecken, besonders der dunkle rechts unten. Ersetzt +durch einen durchgehenden Verlauf über die volle Breite: `.cta-band` in `shared-styles.css`, von +Anthrazit (`--primary-dark`) diagonal in das Dynamic Blue (`--secondary-dark`). Die Tiefe kommt +aus dem Verlauf selbst, die Kontur aus zwei Haarlinien – eine oben über die Breite, eine als +Auftakt über der Überschrift, beide zu den Seiten auslaufend. Buttons und Fußzeile dabei +zurückhaltender: dünnerer Rahmen, weicherer Schatten. + +> Der Verlauf liegt bewusst als CSS-Klasse vor und nicht als Utility-Kette im Blade: er arbeitet +> mit den Theme-Variablen und bleibt so über alle Themes hinweg stimmig. + +Die Textinhalte kommen unverändert aus dem CMS (`cta_title` / `cta_text`) – Punkt 4 der Liste war +inhaltlich bereits durch die neuen Texte erledigt („Ihre Küche. Persönlich geplant."). + +#### 5 · Akkordeons weicher (Nachreichung aus derselben Runde) + +Rückmeldung: Die Klappelemente springen zu schnell auf und zu, der Hover-Effekt ist zu stark. + +Ursache war `x-show` mit einer Opacity-Bewegung: Das Element nimmt sofort seine volle Höhe ein +und blendet nur den Inhalt ein – es sieht aus wie ein Sprung. Ersetzt durch eine Animation von +`grid-template-rows: 0fr → 1fr` über 500 ms mit `cubic-bezier(0.33, 1, 0.68, 1)`, die die +tatsächliche Höhe bewegt. Das Alpine-Collapse-Plugin ist in diesem Projekt nicht registriert; +diese Lösung kommt ohne neue Abhängigkeit aus. + +- Der zugeklappte Zustand steht **auch statisch im `class`-Attribut**, damit vor dem Start von + Alpine nichts aufgeklappt aufblitzt. Die Objekt-Schreibweise von `x-bind:class` entfernt ihn + beim Öffnen wieder. +- `aria-hidden` hängt am Zustand, weil der Inhalt jetzt dauerhaft im DOM steht. +- **Hover:** Die Flächenaufhellung (`hover:bg-muted/30`) ist raus. Geblieben ist ein feiner + Farbwechsel der Überschrift auf die Sekundärfarbe. Im FAQ entfällt zusätzlich der + Hintergrundwechsel am Icon-Kreis. +- Der Pfeil dreht in derselben Dauer und Kurve wie das Aufklappen, damit beides zusammenläuft. + +Betrifft **beide** Akkordeons – die Vertiefungen auf den Markenseiten und das FAQ; sie hatten +dasselbe Verhalten. + +#### 6 · Markenlogos (Zulieferung 03.09.2026) + +Grundlage: `dev/2026-08-28/markenlogos/` – CABINET und Rotpunkt als SVG, Febrü als PNG. Sie +liegen jetzt unter `public/img/assets/marken/` und hängen als `logo` an der jeweiligen Marke. + +| Wo | Vorher | Jetzt | +|---|---|---| +| Kopf der Markenseite | Schriftzug als `

` | Logo **anstelle** des Schriftzugs, im `

` | +| Kachel auf der Startseite | ohne Logo | Logo über Name und Claim | +| Block auf `/marken` | ohne Logo | Logo über Name und Claim | +| „Weitere Marken" am Seitenfuß | ohne Logo | Logo über Name und Claim | + +Das Logo sitzt **im** `

`, nicht daneben: So bleibt die Seitenüberschrift erhalten, den Text +liefert das `alt`-Attribut. Fehlt ein Logo, erscheint weiterhin der Schriftzug – ein Test hält +beide Wege fest. + +**Feste Box statt fester Höhe.** Die drei Logos haben sehr unterschiedliche Seitenverhältnisse +(Febrü 2,3:1, Rotpunkt 1,2:1). Mit einer reinen Höhenangabe hätte Febrü doppelt so breit gewirkt +wie Rotpunkt. Jedes Logo bekommt deshalb Höhe **und** Maximalbreite plus `object-contain` – die +Konvention aus der Sponsoring-Sektion. + +**Alle drei Dateien auf ihren Inhalt beschnitten.** Jede Zulieferung brachte leeren Rand mit, +und zwar unterschiedlich viel – dadurch wirkten die Logos trotz gleicher Box unterschiedlich +groß: + +| Logo | genutzte Höhe der Datei | nach dem Beschnitt | +|---|---|---| +| CABINET | **29 %** | 98 % | +| Rotpunkt | 70 % | 98 % | +| Febrü | 27 % | 100 % | + +CABINET ist ein 4:1-Schriftzug, der in einer nahezu quadratischen Fläche saß – im Seitenkopf +blieben davon rund 28 px sichtbare Höhe. Nach dem Beschnitt sind es 69 px bei voller Boxbreite, +also gut zweieinhalbmal so groß. Rotpunkt wächst um gut das Anderthalbfache und steht jetzt auf +derselben Höhe wie Febrü. + +Bei den beiden SVGs genügte dafür der `viewBox` (Ursprung und Maße auf die Tuschekante gesetzt, +plus 1 % Luft); die Pfade selbst sind unverändert. Der Febrü-PNG wurde tatsächlich beschnitten +und auf 760 px Breite gerechnet – 53 KB → 15 KB. Die Originale liegen unverändert in +`dev/2026-08-28/markenlogos/`. + +**Zweite Feinabstimmung nach Sichtung:** CABINET wirkte noch zu groß, Rotpunkt zu klein – +gewünscht waren −15 % und +20 % bei unverändertem Febrü. Rotpunkt füllte die Box bereits ganz +aus, konnte also nur über eine größere Box wachsen. Deshalb: + +- Alle vier Logo-Boxen **20 % höher** (Seitenkopf 96 → 115 px, Kacheln 44 → 53 px, `/marken` + 56 → 67 px). Rotpunkt füllt sie weiter aus und wächst damit um genau 20 %. +- **Febrü** gleicht das über 20 % Luft oben und unten in der Datei aus und rendert unverändert. +- **CABINET** bekommt 18 % Luft rechts – die Box begrenzt den 4:1-Schriftzug über die *Breite*, + nicht über die Höhe. Die Luft sitzt nur rechts, damit das Logo links bündig zum Text darunter + bleibt. + +Nachgerechnet an allen drei Stellen: CABINET 85 %, Rotpunkt 120 %, Febrü 100 %. + +> Die optische Abstimmung steckt bewusst in den **Zeichenflächen der Dateien** und nicht im +> Blade. So gilt sie an allen vier Stellen gleichzeitig, und die Boxgrößen bleiben eine reine +> Layoutentscheidung. Der Merksatz für neue Logos steht im Kopf von `BrandLogoSeeder`: knapp +> beschnitten rendert ein Logo in Rotpunkt-Größe, mit Rand entsprechend kleiner. + +**Nachziehen auf bestehenden Installationen:** `CmsBrandSeeder` überspringt vorhandene Marken, +trägt die Logos also nur bei einer frischen Installation ein. `BrandLogoSeeder` **(neu)** setzt +sie auf bereits angelegten Marken nach – nur das Feld `logo` und nur, wenn es leer ist. Ein +selbst hochgeladenes Logo bleibt damit stehen; mehrfaches Ausführen ist unbedenklich. + +#### Tests + +`BrandPageSectionsTest` und `BrandPageTest` erweitert: + +- CABINET rendert seine Abschnitte und seinen eigenen Abschlussblock. +- Widget-Beschriftung jetzt für alle drei Marken (Datensatz-Test). +- Rotpunkt: der zusammengeführte Abschnitt existiert, der alte ist weg, der doppelte Satz steht + genau einmal auf der Seite. +- Febrü: der Blaue-Engel-Satz behält seinen Produktbezug. +- Der Rückfall auf den generischen Abschlussblock funktioniert weiterhin – **beide Sprachen + müssen dafür leer sein**, sonst greift die Übersetzungs-Fallbackkette und der englische Text + erscheint auf der deutschen Seite. +- Demogalerien tragen keinen „BEISPIEL"-Vermerk mehr, dafür durchgehend eine Kategorie. +- Der Normalizer entfernt den Vermerk, ohne Bild, Kategorie oder Reihenfolge zu verändern. +- Jede Vertiefung rendert zugeklappt und mit der Höhen-Animation; die alte Sprung-Bewegung ist + weg. +- Weder Markenseite noch FAQ verwenden noch den Flächen-Hover. +- Der Abschlussblock trägt das Verlaufsband; die weichen Kreise sind weg. +- Die Ausnahme für `.variante-glass-flow` ist vorhanden – ohne sie steht der Abschluss hellgrau da. +- Das Logo steht im `

` der Markenseite; ohne Logo erscheint weiter der Schriftzug. +- Alle drei Logos erscheinen auf Startseite, `/marken` und den „Weitere Marken"-Kacheln. +- Die Dateien, auf die der Seeder zeigt, liegen tatsächlich unter `public/`. +- `BrandLogoSeeder` überschreibt kein selbst hochgeladenes Logo. +- Die SVGs unter `public/` sind die beschnittenen Fassungen, nicht die Zulieferungen (erkennbar + am `viewBox`-Ursprung, der nach dem Beschnitt nicht mehr `0 0` ist). + +`php artisan test --compact`: **873 grün** (3217 Assertions), Pint sauber, `npm run build:b2in` +gebaut. + +--- + +## 4. Nicht Bestandteil dieses Auftrags ⏸ + +Referenzbereich voll ausgebaut · 16:9-Display-Modul · Immobilien-CMS · Dubai-Map · +Markenrichtlinie Fahrzeug/Print · weitere frei anlegbare Rubriken · eigene Videolösung. +**Zusätzlich neu zu bewerten:** B2 (Magazin-CMS ans Frontend anschließen, ≈2–3 Std.) +und B8 (Passwort aus dem Seeder). + +--- + +## 5. Abhängigkeiten + +``` +AP1 (Struktur, Umtextung, B1/B3/B4/B5/B6/B7) + ├─ AP2 (Kernmarken, B9) ──┐ + ├─ AP4 (Display-Spiegelung)│ + ├─ AP5 (Referenzen) │ + ├─ AP6 (Sponsoring) ◄──────┤ braucht B9 + └─ AP3 (Termin-Widget) ◄───┘ Markenauswahl braucht CmsBrand +``` + +**B1 zuerst.** Solange der Seeder überschreibt, ist jede redaktionelle Arbeit flüchtig. + +--- + +## 6. Restrisiken & offene Punkte + +| Punkt | Stand | Umgang | +|---|---|---| +| **Rechtstexte** nennen weiter „Connecting Design and Property", Dubai/Lissabon, Supply-Chain | ⚠️ offen | Formulierung freigeben lassen, dann eintragen – nicht selbst getextet | +| **OG-Bild fehlt komplett** (`b2in-og-default.jpg` existiert nicht) | ⚠️ offen | Motiv wählen, ablegen | +| WhatsApp-Nummer (+49 151 51002992?) | offen | bestätigen lassen; ohne Nummer bleibt der Button unsichtbar | +| Logos, Stimmungsbilder, Galeriefotos der drei Marken | offen | Platzhalter stehen, Marcel trägt im CMS nach | +| Zwei neue Magazin-Beiträge sind Platzhalter | offen | gegenlesen lassen | +| Sponsoren-Kurztexte sind Entwürfe | offen | gegenlesen lassen | +| Vier Dubai-Beiträge bleiben im Magazin | bewusst | gehören zum Immobilienbereich, der bestehen bleibt | +| `ecosystem_stats` behauptet „1,7K+ Partner", „510+ Projekte", „98 %" | ruhend | Sektion wird auf keiner erreichbaren Seite gerendert – vor einer Wiederverwendung prüfen | +| B1/B2 Entscheidung | ✅ erledigt | Redaktionsschutz im Seeder, Magazin am Frontend | +| Logo mit Zusatz „Interior" | ✅ erledigt | Zulieferung eingetroffen und zugeschnitten | +| Sponsoring-Logos uneinheitlich | ✅ erledigt | Kacheln mit weißer Bühne, JPG ohne Transparenz fällt nicht mehr auf | + +**Spielregel aus der Umsetzungsliste:** eine Korrekturschleife je Bereich ist enthalten. +Weitere Runden vorher melden und beziffern. + +--- + +## 7. Definition of Done + +- [x] Alle Live-Seiten laden (`LiveSiteReviewTest` grün) +- [x] Marcel kann Marke anlegen, Logo per Picker setzen, Galerie pflegen – und die Änderung + erscheint auf der Website (Gegenprobe zu B2, getestet in `CmsBrandsIndexTest`) +- [x] Erneuter `CmsContentSeeder`-Lauf löscht keine redaktionellen Inhalte (Gegenprobe zu B1) +- [x] `./vendor/bin/pint` +- [x] `npm run build:b2in` (Build-Verzeichnis `build/b2in`) +- [x] `php artisan test --compact` vollständig grün — 810 Tests, 3037 Assertions +- [ ] Keine Treffer mehr für „Supply-Chain", „Local for Local", „Ökosystem", „Connecting Design" + auf den Live-Seiten. **Stand: 8 statt ursprünglich 90.** Davon + 5 in `/magazin` (die vier Dubai-Beiträge, bleiben bewusst), + 1 in `/privacy` und 2 in `/terms` (Rechtstexte, siehe Abschnitt 0, Schritt 2). + Erst abhakbar, wenn die Rechtstexte freigegeben sind. +- [ ] OG-Bild vorhanden +- [ ] Rechtstexte freigegeben und eingetragen + +--- + +## 7a. Vorschau-Instanz für die Abstimmung + +Für die erste Abstimmung läuft der neue Stand als **eigenständige Vorschau** unter +`preview.b2in.eu` – eigenes Verzeichnis, eigene `.env`, eigene Datenbank. Live bleibt unangetastet. + +**Ablauf Schritt für Schritt: `dev/2026-08-28/preview-deployment.md`.** +Dateien dafür: `dev/2026-08-28/deploy/b2in-interior-update.tar.gz` (geänderte Dateien plus +gebaute Assets) und `deploy/dateiliste.txt`. + +Zwei Punkte, die dabei leicht übersehen werden: + +- **Die Vorschau braucht `DOMAIN_B2IN=preview.b2in.eu` in ihrer `.env`.** Der + `ThemeServiceProvider` setzt `URL::forceRootUrl()` auf die konfigurierte Domain; ein unbekannter + Host fällt auf `APP_URL` zurück. Ohne diesen Eintrag rendert die Vorschau zwar, aber jeder + interne Link springt auf die Live-Seite. +- **Eigene Datenbank, keine geteilte.** `InteriorRelaunchCmsSeeder` überschreibt 23 CMS-Sektionen – + auf der Live-DB ausgeführt wäre der alte Stand weg. + +Neu dafür im Code: `config('app.indexable')`. Außerhalb von `APP_ENV=production` liefert jede Seite +`noindex, nofollow`, damit eine Vorschau nicht mit dem Original um Suchergebnisse konkurriert. +Über `SITE_INDEXABLE` einzeln steuerbar. + +--- + +## 8. Deployment-Checkliste (Livegang) + +> **Diese Liste beim Livegang von oben nach unten abarbeiten.** Sie wird mit jedem Arbeitspaket +> ergänzt – zuletzt aktualisiert nach AP1 und der Display-Umstellung. + +### 8.1 Vorher + +- [ ] **DB-Backup ziehen.** Die Seeder unter 8.2 schreiben in `flux_cms_contents`, + `display_versions` und `display_version_items`. +- [ ] `.env` prüfen: `SEED_ADMIN_NAME`, `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD` gesetzt + (neu seit B8 – vorher stand das Passwort im Repo). +- [ ] Wartungsfenster mit Marcel abstimmen: die Displays holen ihre Konfiguration zyklisch, + ein Neustart ist nicht nötig, aber ein sichtbarer Wechsel mitten im Ladengeschäft schon. + +### 8.2 Deployment + +```bash +php artisan migrate --force # legt u. a. cms_brands an (AP2) + +# 0. Kernmarken anlegen (CABINET, Rotpunkt Küchen, Febrü). +# Überschreibt nichts – bereits gepflegte Marken bleiben unberührt. +php artisan db:seed --class=CmsBrandSeeder --force + +# 1. Neue Texte ins CMS. Überschreibt GENAU die 19 in AP1 geänderten Sektionen, +# alles andere bleibt unangetastet. +php artisan db:seed --class=InteriorRelaunchCmsSeeder --force + +# 2. Display-Branding auf das Interior-Logo vereinheitlichen (Logo je Theme + neuer Claim). +php artisan db:seed --class=DisplayInteriorBrandingSeeder --force + +# 3. Medien-URLs host-unabhängig machen. Ohne diesen Schritt zeigen vier veröffentlichte +# Playlists weiterhin auf b2in.test und laden auf dem Live-Display nicht. +php artisan db:seed --class=DisplayMediaUrlNormalizerSeeder --force + +# 4. Ausführliche Markenseiten (CABINET, Rotpunkt, Febrü). Schreibt NUR tagline, description, +# sections, widget_label, cta_title und cta_text – Bilder, Galerie, Leistungen und +# Reihenfolge bleiben unberührt. +php artisan db:seed --class=BrandPageTextSeeder --force + +# 5. Entfernt den Vermerk „BEISPIEL – “ aus vorhandenen Bildunterschriften (Nachtrag 5). +# Fasst nur den Text an, mehrfaches Ausführen ist unbedenklich. +php artisan db:seed --class=BrandGalleryCaptionNormalizerSeeder --force + +# 6. Markenlogos nachtragen. Setzt `logo` nur, wenn das Feld leer ist – ein im Admin +# hochgeladenes Logo bleibt stehen. Braucht public/img/assets/marken/ aus dem Archiv. +php artisan db:seed --class=BrandLogoSeeder --force + +# 7. Welten-Trennung (Nachtrag 7): Navigation und Inhalte der Immobilien-Welt anlegen, +# Interior-Navigation ohne „Immobilien“ und Magazin-Einleitung ohne Dubai-Bezug setzen. +# Die Migration ordnet die drei Dubai-Beitraege der Immobilien-Welt zu. +php artisan db:seed --class=WorldSplitCmsSeeder --force + +# 8. Vierte Kernmarke Otterpohl, Slug „tischlerei“ (Nachtrag 9 + 15) – legt nur an, was fehlt. +php artisan db:seed --class=CmsBrandSeeder --force + +# 9. Herkunfts-Sektion „Aus der Region gefertigt“ (Nachtrag 10) – legt die Sektion nur an, wenn sie +# fehlt. Entfernt aus einem bestehenden Eintrag nur den veralteten Schlüssel „locations“ (Nachtrag 11). +php artisan db:seed --class=OriginSectionCmsSeeder --force + +# 10. Fertigungsort je Kernmarke (Nachtrag 11) – setzt Ort, Entfernung und Rolle nur bei Marken, +# deren Ort noch leer ist. Die Migration davor legt die drei Spalten an. +php artisan db:seed --class=BrandOriginSeeder --force + +# 11. Otterpohl (Nachtrag 15 + 19): ersetzt den Platzhalter „Tischlerei“ durch Name, Claim, Texte, +# Leistungen, Abschnitte, Abschlussblock und Fertigungsort aus der Zulieferung v2 vom 24.09. +# Überschreibt auch einen Stand mit dem Text vom 17.09. Bild, Galerie und Reihenfolge +# bleiben; das Logo wird nur gesetzt, wenn es leer ist. +# Schritt 4 (BrandPageTextSeeder) bringt das Feedback vom 17.09. für CABINET, Rotpunkt +# und Febrü mit – er überschreibt dort wie dokumentiert die Textfelder. +php artisan db:seed --class=OtterpohlBrandSeeder --force + +# 12. Bilder für CABINET, Rotpunkt, Febrü (Nachtrag 16) und Otterpohl (Nachtrag 19): Titelbild nur, +# wenn keins gepflegt ist; Galeriebilder nur, wenn die Datei noch nicht in der Galerie steht. +# Die WebP-Dateien liegen unter public/img/assets/marken/{cabinet,rotpunkt,februe,otterpohl}/ +# im Repository. +php artisan db:seed --class=BrandImageSeeder --force + +# 13. Demobilder aus den Galerien entfernen (Nachtrag 17) – erkennt sie am Dateinamen des +# Demo-Seeders, echte Bilder bleiben. Nach Schritt 12 ausführen. +php artisan db:seed --class=DemoBrandGalleryCleanupSeeder --force + +# 14. WhatsApp-Business-Nummer 0521-98620100 (Nachtrag 18): ersetzt nur den Platzhalter +# „+49 151 00000000“, die falsche Nummer vom 23.09. (…98610020) oder ein leeres Feld – +# eine im Admin gepflegte andere Nummer bleibt. +php artisan db:seed --class=WhatsappNumberSeeder --force + +# 15. Projekttext auf der Startseite (Briefing 24.09., Punkt 7; Nachtrag 19): ersetzt in +# home/audience_switch nur den Satz „Mit deutscher Vertragssicherheit …“. Idempotent. +php artisan db:seed --class=AudienceSwitchProjectTextSeeder --force + +php artisan config:cache +php artisan view:clear + +# Assets. WICHTIG: `build:web` baut nur die vier Frontend-Themes, NICHT das Portal. +# Fehlt public/build/portal/manifest.json, liefert das Admin-Portal 500. +npm ci +npm run build:web +npm run build:portal +``` + +Danach prüfen, dass alle fünf Manifeste liegen: + +```bash +for d in b2in b2a stileigentum style2own portal; do + test -f "public/build/$d/manifest.json" && echo "$d ok" || echo "$d FEHLT" +done +``` + +### 8.3 Nachher prüfen + +- [ ] Startseite: Reihenfolge, Kernmarken-Anker `#marken`, Sponsoring-Logos +- [ ] Markenseite `/marken` – Aufbau, Merkmale, „Alle Marken ansehen" von der Startseite +- [ ] Kernmarken-Detailseiten `/marken/cabinet`, `/marken/rotpunkt`, `/marken/februe` – je mit + Logo im Kopf, ausführlichem Text, aufklappbaren Vertiefungen und eigenem Abschlussblock +- [ ] Logos erscheinen auf der Startseite, auf `/marken` und in den „Weitere Marken"-Kacheln +- [ ] Auf dem Smartphone prüfen: Das Terminanfrage-Widget trägt auf der Markenseite die längere + Beschriftung und darf keine Inhalte verdecken +- [ ] `/netzwerk`, `/interior`, `/partner`, `/service` leiten auf `/marken` +- [ ] Im Admin unter „Kernmarken" eine Marke ändern und prüfen, dass es auf der Seite ankommt +- [ ] Ankündigungsleiste erscheint (Dismiss-Key hängt seit B3 am Inhalt – wer die alte Leiste + weggeklickt hatte, sieht die neue wieder) +- [ ] Logo in Header, Footer **und** in einer Test-Mail +- [ ] Alte Magazin-URLs `/magazin/1` … `/magazin/5` leiten per 301 auf den Slug +- [ ] `/service` und `/portfolio` leiten um statt 500 zu liefern +- [ ] Terminanfrage-Widget: WhatsApp-Button oben, Link führt auf `wa.me/4952198620100`, darunter + „oder per Formular“ und das Formular +- [ ] Interior-Seiten laufen in „Neu“ (``), kein Farb-Schalter; + `/immobilien` unverändert blau +- [ ] `/entwurf`: Gold-Logo in Kopf und Fuß, Hero mit Goldkante und laufender Slideshow (Marken- + Titelbilder), keine Ankündigungsleiste, ein Button im Hero, Termin-Widget unten rechts +- [ ] `/marken/tischlerei`: Text v2 (drei Abschnitte, acht Leistungen, Abschluss „Das Ganze statt + nur das Einzelne“), Werkstatt-Titelbild, Galerie „Referenzen und Einblicke“ mit den Badges + „Realisiertes Projekt“ und „Aus der Werkstatt“ +- [ ] **Die vier Displays sichten:** „Display 4 – Innen", „Kundenstopper", „Display 2", + „Test-Display". Besonders das Modul „Cabinet-Influencerin" – dessen Video lag auf + `b2in.test`. Ob die Datei auf dem Live-Server überhaupt existiert, ließ sich lokal nicht + prüfen; fehlt sie, muss sie nachgeladen werden. +- [ ] Search Console: die alten Magazin-URLs waren indexiert, die 301 fängt sie ab +- [ ] Search Console: `/netzwerk` war indexiert und leitet jetzt auf `/marken` + +### 8.4 Zurückrollen + +Die Seeder sind nicht umkehrbar. Rückweg ist das Backup aus 8.1 – für die betroffenen Tabellen +genügt ein selektives Zurückspielen von `flux_cms_contents`, `display_versions` und +`display_version_items`. + +### 8.5 Nicht vergessen + +- [ ] Altes Seeder-Passwort rotieren, falls es anderswo benutzt wird – es steht weiterhin in der + Git-Historie (B8). +- [ ] WhatsApp-Nummer bestätigen lassen – erst dann erscheint der Button im Terminanfrage-Widget + und in den CTA-Blöcken (CMS: `global.contact.whatsapp`). +- [ ] `CONTACT_FORM_RECIPIENT` in der Live-`.env` prüfen: dorthin gehen auch die Terminanfragen + aus dem Widget. +- [ ] Datenschutzerklärung um einen Absatz zum WhatsApp-Link ergänzen, sobald die Nummer steht. +- [ ] **Rechtstexte:** AGB (`b2in_legal.php` Zeile 117) und Datenschutzerklärung (Zeile 221) + nennen weiter „Connecting Design and Property", Dubai/Lissabon und Supply-Chain-Management. + Von Marcel prüfen lassen, dann eintragen. +- [ ] Prüfen, welches Display auf der Website laufen soll (`referenzen.display_feed.display_id`). + Ohne Eintrag zeigt die Seite das erste aktive Display mit veröffentlichter Playlist. +- [ ] Die zwei neuen Magazin-Beiträge von Marcel gegenlesen lassen – sie sind Platzhalter. +- [ ] **Herstellerfreigabe für die Markentexte** (Rotpunkt, Febrü) einholen – so verlangt es die + Zulieferung selbst. Beide Fassungen nennen zusätzlich die EmpCo-Regeln (ab 27.09.2026): + die Umweltaussagen vorher rechtlich prüfen lassen. +- [ ] **CABINET-Seitentext von Marcel freigeben lassen.** Für CABINET lag keine Zulieferung vor; + der Text ist von uns nach dem Muster der beiden abgestimmten Seiten verfasst und beschreibt + bewusst nur die eigene Arbeitsweise (Nachtrag 5). +- [ ] **Febrü, Vertiefung „Konkrete Standards statt allgemeiner Versprechen“:** Der Blaue Engel + gilt nur für ausgewählte Tischsysteme und Stauraummöbel. Beim Redigieren darf daraus keine + pauschale Aussage über das Gesamtsortiment werden. +- [ ] **Demobilder der Rubrik „Realisiertes Projekt" ersetzen.** Der Vermerk „BEISPIEL" ist auf + Kundenwunsch entfallen (Nachtrag 5); die Stockmotive und KI-Renderings aus + `DemoBrandGallerySeeder` geben sich damit als echte Referenzen aus. Entweder durch echte + Projektfotos ersetzen oder auf „Inspiration" umstellen. +- [ ] **Rotpunkt, Vertiefung „Zahlen, Zertifizierungen und Raumklima“:** Die Aussage zur jährlich + erhaltenen Waldfläche von ungefähr 50 Fußballfeldern darf laut Zulieferung nur mit + ausdrücklicher Bestätigung veröffentlicht werden. Ohne Bestätigung den Absatz entfernen + (Admin: Kernmarken → Rotpunkt → Abschnitte → Vertiefung). +- [ ] OG-Bild anlegen: das Layout verweist auf `img/assets/b2in-og-default.jpg`, die Datei + existiert nicht. Bis dahin zeigt jede geteilte Seite eine Vorschau ohne Bild. +- [ ] Sponsoring-Logo `tc-bad-waldliesborn-horn` liegt nur als JPG vor, also ohne transparenten + Hintergrund. Falls Marcel eine PNG-Variante hat, austauschen. +- [ ] Markengalerien vor dem Livegang durchsehen: jedes Bild muss in der richtigen Rubrik stehen. + Ohne gesetzte Kategorie erscheint ein Bild als „Visualisierung" – für ein echtes Projektfoto + also bewusst „Realisiertes Projekt" wählen. + +--- + +## Nachtrag 6 · Arbeitsplan 14.09.2026, Punkt 1.1: Farbwelt und Logo (14.09.2026) + +Grundlage: `umsetzungen-14-09-2026.md` (freigegebener Umfang Phase 1) und die Shop-Screenshots +unter `_b2in_interior_shop/screenshots/web/` als optische Richtung: warme Creme-/Sandflächen, +dunkles Tannengrün als Kontrast, Serifen-Überschriften, ruhige Flächen ohne Leuchteffekte. + +### Zwei Welten auf einem Themensystem + +`theme-b2in.css` definiert die **Interior-Welt als Standard** auf `:root` und die +**Immobilien-Welt** als Override unter `.world-immobilien` – dieselben Token-Namen, andere Werte. +Das Layout setzt die Welt als Body-Klasse (`world-interior` / `world-immobilien`), ermittelt +über `b2in_world()` in `app/helpers.php`: Immobilien sind alle Routen `immobilien*` und +`dev.immobilien*`, alles andere ist Interior. Bestehende Sektionen mussten dafür nicht angefasst +werden – sie nutzen bereits die Tokens (`text-secondary`, `bg-accent`, `cta-band` …). + +| Token | Interior (neu) | Immobilien (wie bisher) | +|---|---|---| +| `--background` | Creme `#f7f4ef` | helles Beige `#f5f4f2` | +| `--foreground` | Anthrazit `#2d2926` | `#2a2a2a` | +| `--primary` (Buttons, dunkle Sektionen, Wortmarke) | Tanne `#202c2b` | Anthracite `#2b3f51` | +| `--secondary` (Akzent, Hervorhebungen, Logo-Bogen) | Ocker `#d39e46` | Dynamic Blue `#20a0da` | +| `--accent` (Sektionswechsel) | Sand `#d9cbbf` | helles Grau | +| `--clay` (neu) | Clay `#a38f82` | gedämpftes Blau | +| `--footer-*` (neu) | Sand mit Tanne | Anthracite mit Weiß | +| `--cta-band-end` (neu) | helleres Tannengrün (Ton in Ton, Korrektur 15.09.) | Dynamic Blue | +| `--font-secondary` (Überschriften) | EB Garamond (liegt lokal vor) | IBM Plex Sans | + +**Kontrast-Entscheidungen:** Ocker trägt keinen hellen Text (Creme auf Ocker ≈ 2,5:1). Deshalb +ist Ocker in der Interior-Welt Akzent, kein Flächenton unter Text: der Kontakt-Button im Kopf +und der zweite Hero-Button laufen jetzt auf Tanne (gefüllt bzw. als Kontur, `.world-interior +.btn-secondary-accent`), der Abschluss-CTA der Startseite liegt auf dem Abschlussband +(`cta-band`) mit weißem Button – wie auf den Markenseiten. **Korrektur 15.09.:** Der Verlauf +Grün → Ocker wirkte unharmonisch; das Band läuft jetzt Ton in Ton in ein helleres Tannengrün aus, +Ocker sitzt nur noch in der Haarlinie oben. Hervorgehobene Wörter im Bandtitel („Ihr nächster +Schritt") tragen im CMS `text-primary` und waren auf dem dunklen Band unlesbar – in beiden +Welten. `.cta-band .text-primary` setzt sie auf den Akzent der Welt (Ocker bzw. Blau). Ocker-Text +im Footer auf Sand nutzt die tiefe Stufe `--secondary-deep`. `--muted-foreground` ist ein +abgedunkelter Clay, damit Fließtext auf Creme lesbar bleibt (Clay pur ≈ 2,9:1). + +**Aufgeräumt:** Die Glow- und Schattenklassen in `shared-styles.css` (`glow-*`, `shadow-elegant`, +`shadow-premium`, Button-Hover im Glass-Flow) hatten ein festes Blau (`rgba(0,136,204)`, +`hsla(199 …)`) – jetzt hängen sie am Akzent der Welt. Karten im Glass-Flow nutzen `--card` +statt festem Weiß. + +### Logo je Welt + +Zulieferung 07.09.2026 (`dev/2026-08-28/logos/`): das Interior-Logo in Tanne mit Ocker-Bogen +(positiv) bzw. Creme mit Ocker-Bogen (negativ) und das B2In-Logo ohne Zusatz mit blauem Bogen. +Beide Familien sind auf den Inhalt beschnitten (Interior `viewBox="152.5 94.5 289 228"`, +B2In `viewBox="4.5 2 301 176"`) und liegen als SVG und PNG (240 px hoch) unter +`public/img/logos/`; die Interior-Varianten zusätzlich unter `public/_cabinet/assets/` für das +Display. `ThemeHelper::getLogoPath($type, $world)` liefert die Datei je Welt. + +Header und Footer halten Welt und Logo-Pfad in `mount()` fest, weil ein späterer +Livewire-Request keine Seitenroute mehr trägt. Der Interior-Footer liegt auf Sand und zeigt das +**positive** Logo; der Immobilien-Footer bleibt dunkel mit dem negativen. + +**Favicons** (`public/favicon/`, alle Größen plus `favicon.ico`) neu aus der Wortmarke in +Tanne/Ocker erzeugt; `theme-color` auf Creme. Die bisher tote Referenz +`img/favicons/b2in-favicon.ico` (`ThemeHelper::getFaviconPath()`) existiert jetzt. +Mail-Kopf auf Tanne. + +### Welten-Wechsler + +Kopfzeile (Desktop rechts neben dem Sprachwechsel, mobil als erster Block im Menü) und Footer +tragen einen Umschalter „Interior | Immobilien" (`b2in_worlds()`, Labels in `ui.php` de/en). +Die aktive Welt ist gefüllt und trägt `aria-current="page"`. + +### Tests + +`tests/Feature/ColorWorldsTest.php` (neu, 14 Fälle): Body-Klasse und Logo je Welt für +Startseite, `/immobilien`, Detailseite und weitere Seiten; `ThemeHelper::getLogoPath()` je Welt +und für Fremd-Themes; Dateien und Farbwerte beider Logo-Familien; Wechsler in Kopf und Fuß mit +korrekter Markierung; Tokens im Theme; kein festes Blau mehr in den Effekten. +`HomePageStructureTest` an den neuen Zuschnitt und den Sand-Footer angepasst. + +### Offen nach diesem Nachtrag + +- **Freigabe Marcel:** Farbwerte sind laut Plan „blockierend für 1.1" und wurden wie geliefert + umgesetzt. Die **Serife für Überschriften** (EB Garamond) ist eine Ableitung aus den + Shop-Screenshots, kein Planpunkt – ein Token in `theme-b2in.css`, bei Ablehnung in einer Zeile + zurück auf IBM Plex Sans. +- **Bildwelt:** Die Farbwelt wirkt erst mit warmem Bildmaterial (Hero, Kacheln) vollständig; die + Bestandsbilder aus der Immobilien-Zeit sind kühl. Zulieferung weiterhin offen. +- **Display-Player:** nutzt die neuen Interior-Dateien automatisch; der Theme-Scrim im Player + ist weiter neutral dunkel, keine Anpassung an Tanne vorgenommen. +- Nächste Schritte laut Reihenfolge im Arbeitsplan: 1.2 Tischlerei-Markenseite (blockiert durch + Name, Logo, Bilder), 1.3 Herkunfts-Sektion (blockiert durch CABINET-Klärung), 1.4 Akkordeon + und Widget-Beschriftung. + +--- + +## Nachtrag 7 · Saubere Trennung der beiden Welten (14.09.2026) + +Abgestimmt mit Kevin am 14.09.: Nach der Farbwelt-Umstellung zeigte die Immobilien-Welt noch die +Interior-Navigation, und das Magazin mischte Dubai- und Einrichtungsthemen. Entscheidung: **jede +Welt hat ihre eigene Navigation, der Welten-Wechsler ist die einzige Brücke.** Umfang ist eine +Erweiterung zu 1.1 des Arbeitsplans (nicht im freigegebenen Plan enthalten, Aufwand ca. 6–9 h). + +### Was je Welt getrennt ist + +| Bereich | Interior | Immobilien | +|---|---|---| +| Navigation (CMS) | `global.header`: Home · Marken · Referenzen · Magazin · Über B2in | `global.header_immobilien`: Immobilien · Projekte · Magazin · Über B2in | +| Ankündigungsleiste (CMS) | `global.announcement_bar`: aktuelles Interior-Thema (Rotpunkt) | `global.announcement_bar_immobilien`: aktuelles Projekt (Azizi Creek Views 4, wie vor dem Relaunch), eigener Dismiss-Schlüssel | +| Magazin | `/magazin` – nur Interior-Beiträge | `/immobilien/magazin` – nur Immobilien-Beiträge | +| Über uns | `/about` (unverändert) | `/immobilien/ueber-b2in` – Inhalte aus der Zeit vor dem Relaunch, leicht angepasst, als „B2in Immobilien" gekennzeichnet, Claim „Connecting Design and Property" | +| FAQ | `/faq` (unverändert) | `/immobilien/faq` – Fragen zu Investment, Reisen, Steuern, Einrichtung nach dem Kauf | +| Kontakt | `/contact` (unverändert) | `/immobilien/kontakt` – gleiche Seite, eigener Einstieg und eigene Betreffe (Overlay `contact_form_immobilien` über `contact_form`), Mail-Betreff „Kontaktanfrage B2in Immobilien: …"; Kontakt-Button im Kopf und CTAs der Immobilienseite zeigen dorthin | +| Footer-Links | wie bisher | FAQ, Über uns und Magazin zeigen auf die Immobilien-Seiten | +| Terminanfrage-Widget | Anliegen offen | Anliegen „Immobilien" vorbelegt, Mail-Betreff „Terminanfrage B2in Immobilien" | + +**Gemeinsam bleiben** Rechtstexte und die Cookie-Seiten. (Kontakt war zunächst gemeinsam, seit 15.09. je Welt.) + +### Magazin: Beiträge haben eine Welt + +Neues Feld `world` (`interior` | `immobilien`) an `cms_articles`, im Admin als Auswahl „Welt" +mit Badge in der Liste. Die Migration ordnet die drei Dubai-Beiträge (Escrow, Al Jaddaf, +Turnkey) der Immobilien-Welt zu; der Supply-Chain-Beitrag bleibt laut Abstimmung unter B2B in +Interior. `CmsArticle::forFrontend($world)` filtert, Lang-Fallback inklusive. + +Die **Detailseite behält ihre URL** (`/magazin/{slug}`, SEO und 301-Weiterleitungen bleiben), +rendert aber in der Welt ihres Beitrags: die Route setzt die Welt per `b2in_world_set()` vor dem +Rendern, weil Kopf- und Fußzeile sie in `mount()` festhalten. Verwandte Beiträge und der Rückweg +zur Liste bleiben in derselben Welt. + +### Technik + +- `b2in_world()` prüft zuerst einen expliziten Override (`b2in_world_set()`), dann die Route. +- Header: Navigation je Welt; der aktive Pfad wird in `mount()` festgehalten (vorher ging die + Markierung bei jedem Livewire-Request verloren) und nur der spezifischste Eintrag ist aktiv + (`/immobilien/magazin` markiert „Magazin", nicht „Immobilien"). +- `AboutHero`, `OurStory`, `OurValues` nehmen jetzt wie `ImageBreak` einen Sektionsschlüssel. +- Kontaktformular: Betreffe in drei Gruppen (Interior · Immobilien · B2in, `form.subject_groups`). + Die Kernmarken aus dem CMS stehen automatisch am Anfang der Interior-Gruppe, die Welt der Seite + bestimmt, welche Gruppe oben steht. Betreff wird jetzt gegen die Liste validiert. Ältere + CMS-Stände mit flacher `subjects`-Liste funktionieren weiter (Fallback ohne Gruppen). +- Abschluss-CTA („Ihr nächster Schritt") je Welt: `global.cta_section_immobilien` mit eigenem Text + und Link auf `/immobilien/kontakt`; greift automatisch auf allen Seiten der Immobilien-Welt. +- Neue CMS-Sektionen (alle im Admin pflegbar): `header_immobilien`, `announcement_bar_immobilien`, `contact_form_immobilien`, `cta_section_immobilien`, `magazin_list_immobilien`, + `faq_immobilien`, `immobilien_about_hero`, `immobilien_our_story`, + `immobilien_about_image_break`, `immobilien_our_values`. +- `WorldSplitCmsSeeder` legt die neuen Sektionen an und setzt **zwei** Interior-Sektionen aus + den Lang-Dateien zurück: `header` (ohne „Immobilien") und `magazin_list` (ohne Dubai-Bezug). + Lokal ausgeführt, beim Deployment mit erledigen (Abschnitt 8). + +### Tests + +`tests/Feature/WorldSplitTest.php` (neu, 18 Fälle): Navigation je Welt inkl. aktiver Markierung, +Footer-Links, beide Magazinlisten, Detailseite in der Welt des Beitrags, verwandte Beiträge, +CMS-Beiträge je Welt, Editor-Auswahl und -Validierung, Über-uns- und FAQ-Seite der +Immobilien-Welt, Widget-Vorbelegung. `MagazinPageTest`, `HomePageStructureTest`, +`LiveSiteReviewTest` und `CmsAdminTest` an die Trennung angepasst. + +### Offen + +- Navigationspunkt „Projekte" zeigt auf den Anker `#projekte` der Immobilienseite. +- Die Immobilien-Seite selbst nutzt weiter feste Farben (`bg-zinc-950`, Status-Badges in + emerald/sky/amber) außerhalb des Token-Systems – funktioniert in der blauen Welt, ist aber + nicht themenfähig. +- `LiveSiteReviewTest` prüft die Startseite noch auf das Wort „Netzwerk" (steht im Fließtext); + kein Navigationspunkt. + +--- + +## Nachtrag 8 · Terminanfrage-Buttons öffnen das Widget (15.09.2026) + +Auf der Seite gibt es mehrere Sektionen mit „Termin anfragen"-Buttons (Hero, Weiche, Abschluss-CTA, +`/marken`-Abschluss, Markenseiten, Referenzen, Immobilienseite). Bisher führten sie auf die +Kontaktseite. Jetzt öffnen sie das schwebende Terminanfrage-Widget; von einer Markenseite aus ist +die Marke im Anliegen vorbelegt. Die Anliegen-Auswahl des Widgets bleibt wie sie ist (Marken plus +feste Einträge, keine Gruppen – die gibt es nur auf der Kontaktseite). + +**Wie:** Blade-Komponente ``. Zeigt `href` auf `/contact` oder +`/immobilien/kontakt`, trägt der Link `data-open-appointment` (und `data-appointment-topic`); ein +Klick-Delegat in `resources/js/app.js` fängt ihn ab und feuert `open-appointment`, das Widget +öffnet sich, übernimmt das Anliegen (nur wenn es in der Auswahl existiert) und fokussiert das +Namensfeld. Ohne JavaScript bleibt es ein normaler Link; zeigt der CMS-Link woandershin, bleibt +er ein gewöhnlicher Link. Der Kontakt-Button im Kopf bleibt bewusst ein Link zur Kontaktseite. + +**Formularfelder** in Widget und Kontaktformular tragen jetzt die tokenbasierten Klassen +`.form-field` / `.form-select` (shared-styles.css): Kartenton als Grund, Fokusring im Akzent der +Welt, eigener Chevron und lesbare Gruppenüberschriften in der Betreff-Auswahl – statt +Tailwind-Grau. + +**Tests:** `tests/Feature/AppointmentTriggerTest.php` (9 Fälle). + +--- + +## Nachtrag 9 · Tischlerei als vierte Kernmarke, Platzhalter (15.09.2026) + +Arbeitsplan 14.09., Punkt 1.2. Name, Logo, Basisinfos und Bilder fehlen noch (blockierend laut +Plan), Platzhalter sind ausdrücklich erlaubt. Die Marke ist deshalb als **Denkanstoß** angelegt: + +- **Marke `tischlerei`** im `CmsBrandSeeder` (vierter Eintrag, wird bei bestehenden Marken nicht + doppelt angelegt). Inhalte entlang der Ausrichtung aus dem Plan – Aufbau, Sonderlösungen, + Veredelung, Maßarbeit; ergänzt CABINET und Rotpunkt, steht nicht daneben; Werkstatt in + Ostwestfalen ohne Nennung des Partnerbetriebs. Drei Abschnitte mit Vertiefungen, eigener + Abschlussblock, Widget-Beschriftung „Persönliche Beratung zur Maßarbeit vereinbaren“. +- **Ohne Logo**: der Seitenkopf zeigt den Schriftzug, Kacheln kommen ohne Logo aus. Kein + Stimmungsbild, die Kacheln zeigen den Platzhalter. +- **Kachelraster** der Startseite: `sm:grid-cols-2` – 2×2 auf allen Breiten. Vier nebeneinander + war kurz drin, wurde aber als zu schmal verworfen (Abstimmung 15.09.). „Weitere Marken“ auf den + Detailseiten: drei nebeneinander ab `lg`. +- Automatisch dabei: `/marken`, Anliegen im Widget, Betreff-Gruppe Interior der Kontaktseite. +- Seitentitel von `/marken` nennt die vierte Marke. +- Lang-Fallback (`core_brands.brands`) um die Tischlerei ergänzt. + +**Lokal ausgeführt:** `php artisan db:seed --class=CmsBrandSeeder` (legt nur die fehlende Marke an). +Beim Deployment mit erledigen (Abschnitt 8). + +**Was Marcel nachträgt (alles im Admin):** Name der Marke, Logo, Stimmungsbild, echte Texte – +die Platzhalter sind bewusst so geschrieben, dass sie ohne Änderung nicht live gehen sollten +(„Tischlerei“ ist kein Markenname). Slug `tischlerei` bleibt stabil, auch wenn der Name kommt. + +**Tests:** `tests/Feature/FourthBrandTest.php` (7 Fälle). + +--- + +## Nachtrag 10 · Herkunfts-Sektion „Aus der Region gefertigt“ (15.09.2026) + +Arbeitsplan 14.09., Punkt 1.3. Die Klärung „CABINET Hersteller oder Vertrieb“ kam am 15.09. von +Kevin nach seinem Verständnis: CABINET **fertigt selbst in Köln** und **vertreibt über die +CABINET Stores**; der Store Bielefeld ist einer davon. Dort laufen Beratung und Planung für +Bielefeld und Umkreis, gefertigt wird in Köln, aufgebaut beim Kunden – und hier kommt die +Tischlerei ins Spiel, die den Aufbau übernimmt und Sonderwünsche umsetzt. ⚠️ **Von Marcel +bestätigen lassen** (bietet CABINET selbst Aufbau an? Läuft der Aufbau immer über die eigene +Tischlerei?). Die Sektion ist so formuliert, dass sie diesem Verständnis folgt. + +### Aufbau + +- **Neue Sektion `origin_section`** (`OriginSection`, CMS-Gruppe `home`), eingebunden auf der + Startseite zwischen Kernmarken und „Gebaut auf Expertise“ sowie auf `/marken` zwischen + Markenübersicht und „Mehr als Marken: unser Netzwerk“. Anker `#herkunft`. +- **Schematische Karte** (SVG, ab `md`): Bielefeld als Drehscheibe in Ocker, die vier Standorte + als Punkte in Tanne mit gestrichelten Speichen und Entfernungsangabe, gestrichelte Ellipse + „Ostwestfalen“ um die drei OWL-Standorte. Keine Geografie, nur Richtung und Entfernung. + Positionen und Beschriftungsseite lagen zunächst im Inhalt; seit Nachtrag 11 stehen sie im + Code, die Standorte kommen aus den Marken. Unter `md` entfällt die Karte, die Kacheln tragen alles. +- **Vier Standort-Kacheln** (2×2 ab `sm`): Stadt, Marke, Rolle, Entfernung „von Bielefeld“ und + Link „Zur Marke“. Ursprünglich lagen die Standorte im Sektions-JSON, der Link kam aus den + veröffentlichten Marken. Seit Nachtrag 11 gilt: Standort = Marke, siehe dort. +- **Ablauf-Block „Gefertigt in Köln, geplant in Bielefeld“**: drei nummerierte Schritte am + Beispiel CABINET (Beratung und Planung in Bielefeld → Fertigung in Köln → Aufbau vor Ort mit + Sonderwünschen aus der Tischlerei). Das ist die saubere Trennung „gefertigt“ / „vertrieben“ + aus dem Plan. Entfällt, wenn `steps` im CMS leer ist. +- **Entfernungen** gerundet, Straße ab Bielefeld Mitte: Herford rund 20 km, Bünde rund 30 km, + Rheda-Wiedenbrück rund 35 km, Köln rund 200 km. Bei Bedarf im Admin anpassen. +- Styles `.origin-map-*` in `shared-styles.css` (Token-basiert, in beiden Welten gültig, die + Sektion liegt aber nur in der Interior-Welt). Build `npm run build:b2in` ausgeführt. + +**Lokal ausgeführt:** `php artisan db:seed --class=OriginSectionCmsSeeder` (legt nur an, was +fehlt, überschreibt nichts). Beim Deployment mit erledigen (Abschnitt 8, Schritt 9). + +**Offen:** Bestätigung des CABINET-Ablaufs durch Marcel; Wortlaut der Tischlerei-Kachel folgt +dem Platzhalter aus Nachtrag 9 und wandert mit dem echten Markennamen. + +**Tests:** `tests/Feature/OriginSectionTest.php` (12 Fälle, mit Nachtrag 11 umgeschrieben). + +--- + +## Nachtrag 11 · Standorte an der Marke, Marken in der Sidebar (15.09.2026) + +Prüfung der Inhaltspflege im Admin auf Kevins Frage, ob Marken wie das Magazin einen eigenen +Punkt brauchen. Ergebnis: **Die Markenverwaltung gab es schon** („Kernmarken verwalten“, AP2), +sie war nur nicht in der Sidebar verlinkt – erreichbar allein über die Kachel auf der +CMS-Übersicht. Die Herkunfts-Sektion aus Nachtrag 10 lag dagegen komplett im generischen +Inhalte-Editor, inklusive der Kartenkoordinaten `x`/`y` und der Beschriftungsseite als nackte +Felder. Das war für Marcel nicht pflegbar. Abgestimmt mit Kevin: **Weg 1**, Standort an die Marke. + +### Was sich ändert + +- **Sidebar:** neuer Eintrag „Marken“ unter CMS → Website-Inhalte, zwischen Projekte und + Magazin. Die Gruppe klappt auf der Markenseite jetzt auch auf. +- **Drei neue Felder je Marke** (Migration `add_origin_to_cms_brands_table`, übersetzbar wie die + übrigen Textfelder): `origin_city` (Fertigungsort), `origin_distance` (Entfernung als Text, + „rund 30 km“), `origin_role` (ein Satz zur Rolle). Im Admin als Block „Fertigungsort“ unter + den Stammdaten, mit Hinweis: ohne Ort erscheint die Marke nicht in der Sektion. +- **`OriginSection` liest die Standorte aus `CmsBrand::forFrontend()`**: nur veröffentlichte + Marken mit Ort, in der Reihenfolge der Marken. Eine unveröffentlichte Marke fällt damit ganz + heraus (vorher blieb ihr Standort ohne Link stehen – jetzt gibt es keinen Standort ohne + Marke mehr). Der Link „Zur Marke“ hängt an `has_detail_page`, der Text `link_text` liegt + einmal auf Sektionsebene statt je Standort. +- **Kartenpositionen im Code:** `OriginSection::MAP_POSITIONS`, je Slug `x`/`y`/`label`. Eine + neue Marke mit Ort, aber ohne Position, erscheint in den Kacheln, nicht in der Karte – dafür + muss ein Entwickler eine Position ergänzen. Das ist gewollt: die Karte ist Zeichnung, kein + Inhalt. +- **Sektions-JSON `home/origin_section`** enthält nur noch Überschrift, Untertitel, Alt-Text, + Regionslabel, Entfernungs-Suffix, Drehscheibe, Link-Text und Ablauf-Block. Der alte Schlüssel + `locations` wird von `OriginSectionCmsSeeder` aus einem bestehenden Eintrag entfernt, damit er + im Inhalte-Editor nicht mehr als Pflegefeld auftaucht (idempotent, sonst weiterhin nur + anlegen-wenn-fehlt). +- **Lang-Fallback:** die vier Einträge in `core_brands.brands` tragen jetzt `slug` und die drei + Herkunftsfelder, damit die Sektion auch ohne gepflegte Marke steht – dann ohne Links, wie die + Kacheln der Kernmarken. +- **Seeder:** `CmsBrandSeeder` bringt die Herkunft für frische Installationen mit; neu + `BrandOriginSeeder` trägt sie bei bestehenden Marken nach, nur wenn der Ort leer ist (Muster + wie `BrandLogoSeeder`). Keine Dubletten möglich, beide arbeiten über den Slug. +- Ohne Marke mit Ort bleiben Überschrift und Ablauf-Block stehen, Karte und Kacheln entfallen. + +**Lokal ausgeführt:** `php artisan migrate`, `db:seed --class=BrandOriginSeeder` (4 gesetzt), +`db:seed --class=OriginSectionCmsSeeder` (`locations` entfernt). Beim Deployment: Abschnitt 8, +Schritte 9 und 10, die Migration läuft mit `migrate` vorweg. + +**Offen:** wie Nachtrag 10 (Bestätigung des CABINET-Ablaufs durch Marcel). Der generische +Inhalte-Editor bleibt für die übrigen Sektionen ohne Schema; wo Listen mit technischen Feldern +auftauchen, ist derselbe Weg möglich: Daten an ein Modell hängen, Darstellung in den Code. + +**Tests:** `tests/Feature/OriginSectionTest.php` (15 Fälle), drei neue Fälle in +`tests/Feature/Admin/Cms/CmsBrandsIndexTest.php` (Herkunft je Sprache speichern, Ort leeren, +Sidebar-Link). + +--- + +## Nachtrag 12 · Inhalte-Editor: Seitenbaum und Vorschau (15.09.2026) + +Kevins Rückmeldung nach der Prüfung des CMS: der Punkt „Inhalte“ zeigt Datenbank-Gruppen +alphabetisch, ohne Bezug zur Seite, mit JSON-Schlüsseln als Beschriftung – zu abstrakt für +Marcel. Abgestimmt: Stufe 1 (Seitenbaum, Klartext) und Stufe 2 (Vorschau im Rahmen) umsetzen, +Stufe 3 (Stift-Markierungen in der Vorschau, die den Editor öffnen) erst nach Marcels Feedback. + +### Stufe 1 · Seitenbaum statt Gruppen + +- **Registry `config/cms_pages.php`:** je Seite (Startseite, Marken, Referenzen, Magazin, Über + uns, Kontakt, FAQ, „Auf jeder Seite“, Immobilien-Welt, Rechtliches) die Sektionen in der + Reihenfolge, wie sie auf der Website stehen, mit Titel, einem Satz Beschreibung, Pfad und + Sprungmarke. Dazu ein Wörterbuch `field_labels` (`button_text` → „Button-Beschriftung“, + `x`/`y` → „Position (Entwickler)“ …), je Sektion überschreibbar über `fields`. +- **`App\Services\CmsPageTree`** verbindet die Registry mit der Datenbank: welche Sektion + existiert (`content_id`), welche fehlt, was in der Datenbank liegt, aber auf keiner Seite + eingetragen ist. Letzteres landet als Sammelpunkt **„Nicht mehr verwendet“** ganz unten + (Netzwerk-, Partner- und alte Immobilien-Sektionen, `home/synergie_section` & Co.) – nichts + geht verloren, nichts steht im Weg. +- **Editor (`admin.cms.content-index`):** linke Spalte zeigt den Seitenbaum, nummeriert von + oben nach unten. Ein Klick wählt genau eine Sektion (`selectSection`), der Kopf zeigt Titel, + Beschreibung, Seite und den technischen Schlüssel klein darunter. Felder tragen die + Klartext-Beschriftung, der Schlüssel steht als `code` daneben; Typ-Badges heißen „Text“, + „Formatierter Text“, „Bild“, „Liste“. Der Listen-Editor (Overlay) beschriftet seine Felder + genauso. `selectGroup` bleibt für die Gruppenansicht erhalten. +- **Fehlende Sektion anlegen:** steht eine Sektion in der Registry, aber nicht in der + Datenbank (heute z. B. das Terminanfrage-Widget), erscheint sie grau mit „+“. Ein Klick + legt sie über `CmsContentSeeder` mit den Texten aus `b2in.php` an. Rechtliches und die + Kontaktdaten (`global/contact`) haben keine Vorlage, dort erscheint ein Hinweis. + +### Stufe 2 · Vorschau + +- Rechts neben dem Editor ein **iframe mit der echten Website** (Domain aus + `config('domains')`, Pfad der Seite, `?cms-preview=` als Cache-Brecher, `#anker`). Beim + Wechsel der Sektion springt die Vorschau zur Sprungmarke, nach jedem Speichern lädt sie + neu (`previewVersion`). Buttons: neu laden, in neuem Tab öffnen, Vorschau aus/ein. + Keine Anmeldung nötig, die Website ist öffentlich; Frame-Header setzt die App keine. +- **Sprungmarken nachgezogen:** `id` an den Wurzelelementen der lebenden Sektionen (`hero`, + `gruender`, `wege`, `marken`, `herkunft`, `vision`, `engagement`, `displays`, `immobilien`, + `cta`, `seitenkopf`, `netzwerk`, `wachstum`, `marken-cta`, `projekte`, `magazin`, + `geschichte`, `bild`, `werte`, `kontakt`, `faq`, `header`, `footer`). Beim Display-Feed + sitzt der Anker am Wurzel-`div`, weil die Sektion ohne veröffentlichtes Display leer bleibt. +- Sektionen ohne Seite (Beitragsseite, Widget, Rechtliches) zeigen die Seite ohne Sprung. + +### Erkenntnisse beim Kartieren + +- `/immobilien` rendert `web/dev/immobilien-azizi.blade.php` mit statischen Texten aus + `immobilien-azizi.json`; die CMS-Sektionen `immobilien/hero`, `warum_dubai`, `kaufprozess` + usw. gehören zur stillgelegten `web/immobilien.blade.php` und stehen deshalb unter „Nicht + mehr verwendet“. Gleiches gilt für `about/leadership_team`. +- `display_feed` liegt in der Gruppe `referenzen`, steht aber zuerst auf der Startseite – + im Baum unter Startseite mit Hinweis auf /referenzen. + +**Tests:** `tests/Feature/Cms/CmsPageTreeTest.php` (9 Fälle, darunter: jede Sprungmarke der +Registry existiert auf der jeweiligen Seite, jede Sektion mit Vorlage ist in `b2in.php` +auffindbar) und sechs neue Fälle in `tests/Feature/Cms/CmsAdminTest.php` (Seitenbaum, +Sektion wählen mit Klartext-Labels und Vorschau-Adresse, Sammelpunkt, Anlegen aus Vorlage, +Anlegen ohne Vorlage, Vorschau ausblenden). Kein Deployment-Schritt nötig: Registry und +Editor sind Code, die Datenbank bleibt unverändert. + +**Offen:** Stufe 3 nach Marcels Rückmeldung. Neue Sektionen müssen in `cms_pages.php` +eingetragen werden, sonst landen sie unter „Nicht mehr verwendet“ – als Erinnerung steht das +im Kopf der Datei. + +--- + +## Nachtrag 13 · Referenzen als eigenes Modul, Immobilien-Projekte ausgeblendet (15.09.2026) + +Kevin nach der Abnahme von Nachtrag 12: „Projekte“ im Portal (die Immobilienprojekte aus +Dubai) ist nicht freigegeben und kommt raus; an die Stelle gehören die Projekte/Referenzen der +Interior-Welt. Das ist eine **Korrektur zu AP5**, das die Referenzen bewusst aus den +Markengalerien zusammengesammelt hatte, um keine zweite Pflegestelle zu schaffen. Ein +realisiertes Projekt hat aber mehr als ein Bild und oft mehr als eine Marke (CABINET plus +Tischlerei), darum jetzt ein eigener Eintrag. + +### Was sich ändert + +- **Neues Modell `CmsReference`** (Tabelle `cms_references`): Titel und Beschreibung + übersetzbar, Ort und Jahr als Text, Titelbild, weitere Bilder mit Bildunterschrift, + beteiligte Marken als Liste von Slugs aus `cms_brands`, Veröffentlicht, Sortierung. +- **Portal → CMS → „Referenzen“** (`admin.cms.references-index`, Route + `cms.references.index`): Liste mit Vorschaubild, Marken-Badges und Bildanzahl; Formular + nach dem Muster des Magazins mit Sprachumschalter, Marken als Checkboxen, Titelbild und + Galerie über den Media-Picker, Galeriebilder sortierbar. Der Kopf der Seite sagt die Regel: + nur ausgeführte Aufträge, Entwürfe gehören in die Galerie der Marke. +- **Sidebar und CMS-Übersicht:** „Referenzen“ ersetzt „Projekte“. Die Route + `cms.projects.index` bleibt bestehen und ist per Adresse erreichbar, nur nicht mehr + verlinkt – die Dubai-Daten bleiben unangetastet, falls die Freigabe später kommt. +- **`/referenzen`** zeigt die veröffentlichten Referenzen: Titelbild, Titel, „Ort · Jahr“, + Beschreibung, weitere Bilder als kleine Reihe, darunter die beteiligten Marken verlinkt auf + ihre Markenseiten (unveröffentlichte Marken fallen heraus). Ohne Titelbild dient das erste + Galeriebild als Titelbild, ohne jedes Bild wird die Referenz übersprungen. +- **Fallback bleibt:** Solange keine Referenz veröffentlicht ist, greift der Stand aus AP5 + (Bilder der Rubrik „Realisiertes Projekt“ aus den Markengalerien). Der Leerzustand von AP5 + gilt weiter, wenn beides leer ist. So sieht die Preview beim Deployment keinen Unterschied, + bis Marcel die erste Referenz anlegt. +- Registry `cms_pages.php`: Beschreibung von „Realisierte Projekte“ verweist auf das neue Modul. + +**Deployment:** nur `php artisan migrate --force` (Abschnitt 8.2, Schritt 1 deckt die neue +Tabelle ab). Kein Seeder, keine Daten. + +**Offen:** Marcel legt die ersten Referenzen selbst an; die bisherigen Galeriebilder der Marken +bleiben dort, wo sie sind. Ob die Bilder aus den Markengalerien später automatisch als Referenz +übernommen werden sollen, klärt sich mit den ersten echten Einträgen. + +**Tests:** `tests/Feature/Admin/Cms/CmsReferencesIndexTest.php` (9 Fälle), vier neue Fälle in +`tests/Feature/ReferenzenPageTest.php` (Referenzen vor Galerien, Entwurf ausgeblendet und +Fallback, unveröffentlichte Marke und Referenz ohne Bild, Galeriebild als Titelbild), +Dashboard-Statistik in `CmsAdminTest` auf Referenzen umgestellt. + +--- + +## Nachtrag 14 · Bilddauer im b2in-Display mit Nachkommastelle (17.09.2026) + +Kevins Wunsch: Im Store-Display-Modul „b2in Display“ soll die „Dauer (Sekunden)“ eines Bildes +auch 1,5 annehmen. Das Feld war ganzzahlig (`int`, Zahlenfeld mit `min=1`). + +- **Eingabe als Text** mit Dezimal-Tastatur: „10“, „1,5“ und „1.5“ sind gültig, Leerzeichen + egal, gerundet auf Zehntel. Erlaubt sind 0,5 bis 120 Sekunden; alles andere bringt beim + Speichern eine Fehlermeldung am Feld und speichert nichts. Bei Videos wird das Feld ignoriert. +- **Gespeichert wird eine Zahl** (`duration_seconds` = 1.5). Der Player in + `public/_cabinet/b2in/index.html` rechnet ohnehin `duration_seconds × 1000`, kommt also ohne + Änderung mit 1500 ms klar. Bestehende Einträge mit ganzen Sekunden bleiben unverändert. +- Anzeige in der Eintragsliste mit Komma („1,5s“), ganze Zahlen ohne Nachkommastelle. +- Doku `public/_cabinet/_docs/b2in-displays.md` ergänzt. + +**Tests:** vier neue Fälle in `tests/Feature/DisplayVersionTest.php` (Komma-Eingabe, +Parser/Formatierer, Ablehnung unter 0,5 s, Video ignoriert das Feld). Kein Deployment-Schritt. + +--- + +## Nachtrag 15 · Otterpohl als vierte Marke, Feedback 17.09. eingearbeitet (17.09.2026) + +Zulieferung 17.09.2026: `dev/2026-08-28/otterpohl_Webseitentext_FINAL.md` (Text der +Tischlerei-Seite), `dev/2026-08-28/markenlogos/otterpohl.jpg` (Logo) und +`dev/2026-08-28/feedback-17-09-2026.md` (Marcels Rückmeldung zu CABINET, Febrü und Rotpunkt). + +### Otterpohl ersetzt den Platzhalter „Tischlerei“ + +- **Marke:** Name „Otterpohl“, Claim „Nicht easy to click & buy. Aber easy to get.“, Kacheltext, + Text für /marken, drei Merkmale, Einleitung (erster Block des Textes), vier Leistungen, **13 + Abschnitte** mit den Überschriften der Zulieferung 1:1, die pointierten Schlusssätze als + Schlusszeile je Abschnitt, Abschlussblock „Ihre Idee. Handwerklich umgesetzt.“, Fertigungsort + Rheda-Wiedenbrück mit Rolle „seit 2014 an unserer Seite“. Slug `tischlerei` bleibt (Links, + Widget-Thema, Kartenposition). +- **Logo:** `public/img/assets/marken/otterpohl.png`, aus der JPG-Zulieferung freigestellt und + knapp beschnitten (Imagick), damit es in der gemeinsamen Logo-Box wie die anderen wirkt. + Das Logo trägt „25 Jahre“ – so geliefert, so übernommen. +- **Seeder:** neu `OtterpohlBrandSeeder` (überschreibt bei der bestehenden Marke alle Textfelder, + lässt Bild, Galerie und Reihenfolge stehen, Logo nur wenn leer); `CmsBrandSeeder` nutzt + dieselben Daten für frische Installationen; `BrandLogoSeeder` und `BrandOriginSeeder` kennen + die Marke. Lang-Fallback, Seitentitel von /marken und der Ablauf-Block der Herkunfts-Sektion + nennen jetzt Otterpohl. + +### Feedback 17.09. in den Markentexten (`BrandPageTextSeeder`) + +- **CABINET:** Der Abschnitt „Aufmaß vor Ort statt Schätzung am Telefon“ widersprach Marcels + Ablauf (erst Foto und grobes Maß, Aufmaß erst nach Angebot). Ersetzt durch „Ein Bild und ein + grobes Maß reichen für den Anfang“ mit Marcels Fassung wörtlich und der Ablaufzeile + „So läuft’s: Foto + grobe Maße → … → Bestellung“ als Schlusszeile. Zweiter Absatz von + „Die Fragen, die wir vorher stellen“ ersetzt (Karnevalskostüme, Sportsachen). +- **Febrü:** nur die vereinbarten Stellen: „Bei uns gibt es Kunden – keine Schubladen“ (drei + Absätze), „Aus Herford“ mit „sehr hohe Fertigungstiefe“ statt 95 Prozent, „Wertschätzung ist + keine Frage der Stückzahl“ (Familienalltag statt Mütter-Passage). Der doppelte Schlusssatz + „Ob im Kleinen oder im Großen …“ steht nur noch beim ersten Abschnitt. „Nicht drei Marken“, + „Das Wir und das Ich“ und der mitlaufende CTA unverändert. +- **Rotpunkt:** „Mehr als Küchen“ mit Marcels drei Absätzen ersetzt. +- **Handwerk als gemeinsamer Abschluss (Punkt 3b):** Die markeneigenen Abschnitte + „Schranksystem plus Handwerk“ (CABINET) und „Küchenstudio plus Handwerk“ (Rotpunkt) sind + entfallen. Stattdessen gibt es **eine** Sektion `brand_craft` („Planung plus Handwerk“, Lang- + Datei, im Inhalte-Editor unter Marken → „Handwerk-Abschluss auf jeder Markenseite“ anlegbar), + die `BrandDetail` als letzten Abschnitt auf jeder Markenseite anhängt – außer auf der + Otterpohl-Seite selbst. Der Link „Mehr zu Otterpohl“ erscheint nur, wenn die Marke + veröffentlicht ist. Anker `#handwerk`. Einmal gepflegt, überall gleich – genau wie gewünscht. + +**Lokal ausgeführt:** `OtterpohlBrandSeeder`, `BrandPageTextSeeder`, `BrandLogoSeeder`. +Beim Deployment: Abschnitt 8.2, Schritt 4 (Feedback) und Schritt 11 (Otterpohl). + +**Offen:** Stimmungsbild und Galerie für Otterpohl (im Admin nachtragen); Nachtrag 10 bleibt +offen, ob CABINET selbst Aufbau anbietet. Der Text bestätigt Rheda-Wiedenbrück und die +Montage-Partnerschaft seit 2014 – die Herkunfts-Sektion passt damit. + +**Tests:** `tests/Feature/BrandFeedbackTest.php` (10 Fälle: CABINET-Einstieg und Fragen, Febrü- +Passagen und einmaliger Schlusssatz, Rotpunkt „Mehr als Küchen“, Handwerk-Abschluss auf drei +Seiten mit Link und Position, nicht auf Otterpohl, ohne Link bei unveröffentlichter Marke, +CMS-Vorrang, Otterpohl-Seeder behält Logo/Bild/Reihenfolge); `FourthBrandTest`, +`MarkenPageTest` und `BrandPageSectionsTest` auf die neuen Texte umgestellt. + +--- + +## Nachtrag 16 · Erste Bilder für CABINET und Rotpunkt (22.09.2026) + +Zulieferung 22.09.2026 unter `dev/2026-08-28/images/{cabinet,rotpunkt}/`, das Titelbild je +Marke mit `_title_` markiert. 12 Bilder CABINET, 14 Bilder Rotpunkt, alles Visualisierungen des +Herstellers (Renderings), 3–6 MB je Datei. + +- **Optimierung:** alle Bilder als WebP mit maximal 1920 px Kantenlänge, Qualität 82, ohne + Metadaten (Imagick) nach `public/img/assets/marken/cabinet/` und `…/rotpunkt/`; 55–330 KB je + Datei statt mehrerer MB. Sprechende Dateinamen aus den Motiven („dachschraegenschrank-filo- + cashmere.webp“, „kueche-07.webp“). Die Originale bleiben per `.gitignore` außerhalb des + Repositorys, nur die WebP-Fassungen werden deployt. +- **Titelbild** (`image`): CABINET „Ankleide Solitär, Eiche Tano“, Rotpunkt „Küche 01“. Es + erscheint im Seitenkopf der Markenseite, auf der Startseiten-Kachel und im Markenblock auf + /marken – dort, wo bisher der Platzhalter stand. +- **Galerie:** die übrigen Bilder als **Visualisierung** mit Bildunterschrift aus dem Motiv. + Die Rubrik passt: Badge „Visualisierung“, Hinweis „Entwürfe und Visualisierungen“. Unter + „Referenzen“ erscheinen sie damit bewusst nicht – das sind keine von uns ausgeführten Projekte. + Marcel kann jedes Bild im Admin umkategorisieren, sobald ein echtes Projekt dahintersteht. +- **`BrandImageSeeder`:** Titelbild nur, wenn keins gepflegt ist; Galeriebilder nur, wenn die + Datei noch nicht in der Galerie steht; vorhandene Einträge bleiben samt Kategorie und + Unterschrift. Mehrfach ausführbar. Febrü und Otterpohl bleiben unberührt (keine Zulieferung). + +**Lokal ausgeführt:** `php artisan db:seed --class=BrandImageSeeder`. Beim Deployment: +Abschnitt 8.2, Schritt 12 – die Dateien kommen mit dem Repository. + +**Ergänzung 23.09.2026:** Febrü nachgeliefert (`dev/2026-08-28/images/februe/`, 16 Bilder, +Titelbild „Base Thekenlösung“). Gleiche Behandlung, Bildunterschriften nach den Febrü- +Produktlinien aus den Dateinamen („Active 2.0 mit Bureo“, „Arena Stage“, „Vio“ …). Der Seeder +und `BrandImageSeederTest` decken jetzt drei Marken ab. + +**Offen:** Bilder für Otterpohl; Freigabe der Bildunterschriften durch Marcel +(aus den Dateinamen abgeleitet, z. B. Frontdekore „Filo Cashmere“, „Velluto Fango“, bei Febrü +die Produktlinien). + +**Tests:** `tests/Feature/BrandImageSeederTest.php` (4 Fälle: Titelbild und Galerie, jede +referenzierte Datei existiert als optimierte Datei unter 400 KB und 1920 px, zweiter Lauf +lässt Gepflegtes stehen, Anzeige auf Startseite, /marken, Markenseite und nicht unter Referenzen). + +--- + +## Nachtrag 17 · Demobilder raus, Galerie ohne Rubrikwechsel (22.09.2026) + +Abstimmung 22.09. nach den ersten echten Bildern: die Demobilder aus AP2 müssen weg, der +Rubrikwechsel „Realisierte Projekte / Inspirationen“ ist ausgesetzt, solange es keine +realisierten Projekte gibt, und die zwei Zeilen unter der Galerie-Überschrift werden eine. + +- **`DemoBrandGalleryCleanupSeeder`** entfernt die elf Demobilder (CABINET 4, Rotpunkt 3, Febrü + 4) aus den Galerien. Erkennung am Dateinamen aus `DemoBrandGallerySeeder`, nicht an der + Unterschrift – den Vermerk „BEISPIEL“ hatte Schritt 5 auf der Vorschau bereits entfernt. + Alle anderen Bilder bleiben. Idempotent. +- **Galerie** auf der Markenseite: ein Raster, realisierte Projekte zuerst, jedes Bild trägt + weiter sein Badge („Realisiertes Projekt“ / „Visualisierung“). Kein Tab-Wechsel, keine + Gruppenüberschriften, kein separater Hinweistext. Die Kategorie im Admin bleibt bestehen, + der Wechsel lässt sich wieder einschalten, sobald echte Projekte da sind. +- **Texte:** Überschrift „Inspirationen und Entwürfe“, eine Zeile darunter: „Visualisierungen, + die zeigen, was in einem Raum möglich ist – noch bevor das erste Möbel steht.“ (vorher + „Referenzen und Inspirationen“ plus Hinweis unter der Rubrik). + +**Lokal ausgeführt:** `php artisan db:seed --class=DemoBrandGalleryCleanupSeeder` (11 entfernt). +Beim Deployment: Abschnitt 8.2, Schritt 13, nach Schritt 12. + +**Tests:** `BrandPageTest` umgestellt (ein Raster mit Badges und Projekten zuerst, genau eine +Zeile unter der Überschrift, Aufräum-Seeder entfernt nur Demobilder und ist idempotent). + +## Nachtrag 18 · WhatsApp zuerst, Business-Nummer, Farbvariante „Modern“ (23.09.2026) + +Rückmeldung von Marcel über Kevin am 23.09.: Im Termin-Widget soll WhatsApp oben stehen und das +Formular darunter; als Nummer gilt die Business-Nummer 0521-98620100 (am 23.09. kam zunächst +…98610020, Korrektur von Marcel am 24.09.). Außerdem wirkt ihm die +Farbwelt „zu altbacken“ – die beigen Flächen. Gewünscht ist eine klarere, modernere Variante, die +farblich am Logo bleibt (Tanne und Ocker unverändert), sowie ein schwimmender Schalter, um beide +Stände direkt zu vergleichen. + +### Termin-Widget + +- **Reihenfolge:** WhatsApp-Block direkt unter der Einleitung, darunter eine Trennzeile „oder per + Formular“, dann das Formular. Einleitung jetzt „Am schnellsten per WhatsApp – wir bereiten die + Nachricht mit Ihren Angaben vor.“ Der Hinweis, dass der Besucher selbst abschickt, bleibt. + Der vorbereitete Text liest weiter Anliegen, Wunschtermin und Name aus dem Formular, auch wenn + es jetzt unter dem Button steht. Ohne Nummer im CMS erscheint weder Button noch Trennzeile. +- **Nummer:** `WhatsappNumberSeeder` trägt `+49 521 98620100` in `global.contact.whatsapp` ein + (de und en), aber nur, wenn das Feld leer ist, noch den Platzhalter „+49 151 00000000“ aus der + ersten Einrichtung enthält oder die falsche Nummer vom 23.09. (`+49 521 98610020`) – die hatte + Marcel versehentlich geschickt, auf der Vorschau lief der Button deshalb ins Leere. Eine im + Admin gepflegte andere Nummer bleibt stehen. Fehlt der Eintrag, wird er angelegt. Idempotent. + Ergebnis-Link: `https://wa.me/4952198620100`. Auf einer Vorschau ohne Portal reicht + `php artisan db:seed --class=WhatsappNumberSeeder --force`. + +### Farbvariante „Modern“ + +Eine zweite Interior-Palette als Override in `theme-b2in.css` (`.palette-modern .world-interior`). +Sie ändert **nur Flächen**, keine Logofarben: + +| Token | Bisher (Creme) | Modern | +|---|---|---| +| Hintergrund | Creme `#f7f4ef` | fast weiß, leicht warm `#fbfaf8` | +| Karten | `#fbf9f5` | reinweiß | +| Sektionswechsel (`bg-accent`) | Sand `#d9cbbf` | helles Grau-Beige `#efece7` | +| Ruhige Flächen (`bg-muted`) | helles Sand | `#f2f0ed` | +| Linien / Eingabefelder | Sand-Töne | `#e6e2dc` / weiß | +| Hero-Bühne | Sand | helles Grau-Beige | +| Footer | Sand mit Tanne | Tanne mit Creme, Ocker als Akzent | +| Tanne, Ocker, Clay, dunkle Sektionen, CTA-Band | unverändert | unverändert | + +Karten liegen jetzt weiß auf fast weiß, deshalb haben `--shadow-card` und `--shadow-elevated` in +der Variante etwas mehr Tiefe. Weil der Footer dunkel ist, trägt er in „Modern“ das **negative +Logo** (Rückmeldung Kevin 23.09.): der Footer rendert beide Logos (`footer-logo-light` / +`footer-logo-dark`), das Stylesheet blendet je Palette das passende ein – die Umschaltung läuft +im Browser, ohne Server-Roundtrip. Die Immobilien-Welt bleibt bei ihrem einen negativen Logo. +Zwei Nachbesserungen nach dem ersten Blick (Kevin 23.09.): Abschlussband und Footer sind in +„Modern“ beide dunkel, deshalb trennt eine helle Haarlinie (`--footer-muted` bei 35 %) den Footer +oben ab. Und der schwebende Termin-Button war als reine Kontur über Bildern und weißen Karten +kaum zu sehen – er trägt jetzt in beiden Paletten eine Fläche in Kartenfarbe +(`.appointment-toggle`), Hover bleibt Tanne. + +Dritte Nachbesserung (Kevin 23.09.): der **Cookie-Button** unten links kam noch in Blau aus +`config/cookie-consent.php` – das ist die Immobilien-Welt. `theme-b2in.css` überschreibt die +Paketvariablen (`--cc-*`) für `.world-interior`: Button in Tanne mit dem Verlauf des +Akzent-Buttons, Icon, Schalter, Fokusring und Link-Hover in Ocker, „Speichern“ in Tanne, +„Akzeptieren“ bleibt grün. Gilt in beiden Paletten; `/immobilien` behält das Blau. Der dunkle Footer ist die eine bewusste Abweichung, die über +„heller“ hinausgeht: er gibt der hellen Seite unten einen klaren Abschluss. Wenn Marcel ihn +lieber hell hätte, sind es vier Token. + +### Schalter + +- `` schwimmt links in der Mitte (unten links sitzt der Cookie-Button, unten + rechts das Termin-Widget): eine senkrechte Pille „Farbwelt · Bisher / Neu“. Nur auf den + Interior-Seiten von b2in, nicht auf `/immobilien`. +- Die Wahl liegt im `localStorage` (`b2in-palette`) und wird schon im `` gesetzt, bevor das + Stylesheet greift – kein Aufblitzen in Creme. `?palette=modern` bzw. `?palette=classic` an einer + URL überschreibt die gemerkte Wahl; damit lässt sich Marcel ein Link auf die neue Variante + schicken: `https:///?palette=modern`. +- Abschalten, sobald entschieden ist: `B2IN_PALETTE_SWITCH=false` in `.env` + (`config('app.palette_switch')`). Dann erscheint weder Schalter noch Skript, die Seite läuft in + der Standard-Palette. Soll „Modern“ dauerhaft werden, werden die Token in `:root` übernommen und + der Override entfernt – ein Nachtrag, kein Umbau. + +**Lokal ausgeführt:** `php artisan db:seed --class=WhatsappNumberSeeder` (2 Sprachen gesetzt), +`npm run build:b2in`. Beim Deployment: Abschnitt 8.2, Schritt 14, dazu `npm run build:web`. + +**Tests:** `WhatsappNumberSeederTest` (6: anlegen, Platzhalter ersetzen, falsche Nummer vom 23.09. +korrigieren, gepflegte Nummer behalten, idempotent, Link auf der Seite), `AppointmentWidgetTest` (+2: WhatsApp vor dem Formular, keine +Trennzeile ohne Nummer), `ColorWorldsTest` (+6: Schalter auf Interior-Seiten, nicht auf +Immobilien, per Config abschaltbar, Override ohne Logofarben, negatives Logo im Footer der +Variante, Immobilien-Footer unverändert). + +**Offen:** Marcel entscheidet zwischen „Bisher“ und „Neu“ – oder benennt, was an „Neu“ noch +fehlt (Footer hell/dunkel, Grad der Aufhellung). Danach Schalter abschalten. + +## Nachtrag 19 · Otterpohl: Text v2 und erste Bilder (24.09.2026) + +Zulieferung vom 24.09.: `dev/2026-08-28/otterpohl_Webseitentext_FINAL_v2.md` löst die lange Fassung +vom 17.09. komplett ab, dazu 13 Bilder in `dev/2026-08-28/images/otten/` (Titel plus 2–5 vom +Unternehmen, der Rest Referenzen). + +### Text v2 + +Deutlich kürzer als v1: eine Einleitung, zwei Abschnitte, acht Leistungen, ein Schlussblock. Die +Fassung vom 17.09. mit zwölf Abschnitten (u. a. „Seit 2014 an unserer Seite“, „Farbe darf auch +genau die Farbe sein“) ist damit vollständig ersetzt. Zuordnung auf die Markenseite: + +| Zulieferung v2 | Feld | Auf der Seite | +|---|---|---| +| „Ziemlich nah an der eierlegenden Wollmilchsau“ + 4 Absätze | `sections[0]`, Claim „Genau dort beginnt der individuelle Innenausbau.“ | erster Abschnitt | +| „Vom einzelnen Möbel bis zum ganzen Raum“ | `sections[1]`, Claim „So können wir Räume ganzheitlicher denken …“ | zweiter Abschnitt | +| „Planung trifft Handwerk“ | `sections[2]`, Claim „Das Ergebnis kann ein einzelnes Möbel sein. Oder ein kompletter Raum.“ | dritter Abschnitt | +| „Leistungen und Möglichkeiten“ (8 Punkte) | `services` | Leistungsraster, zwei Reihen à vier | +| „Das Ganze statt nur das Einzelne“ | `cta_title` / `cta_text` | Abschlussblock hinter Leistungen und Galerie – so bleibt die Reihenfolge der Zulieferung erhalten | +| „Nicht easy click & buy. Aber dennoch easy to get.“ | `tagline` und Ende des `cta_text` | Claim im Seitenkopf und Schlusssatz | + +- **Name** jetzt „Otterpohl Möbelservice“ – so nennt die Zulieferung den Partner durchgängig. + Überall dort, wo der Name aus der Marke kommt (Kacheln, Widget-Anliegen, Herkunfts-Kachel). + Der Link im Handwerk-Abschnitt der anderen Markenseiten heißt weiter „Mehr zu Otterpohl“. +- **Kein „seit 2014“ mehr:** v2 erwähnt die gemeinsame Zeit nicht. Der Hinweis ist aus Teaser, + Fertigungsort (`origin_role`), Lang-Fallback und dem Handwerk-Abschnitt (`brand_craft`) + entfernt. ⚠️ Falls Marcel die Angabe bewusst behalten wollte, ist sie in drei Sätzen zurück. +- `description` bleibt leer, damit der Text in der Reihenfolge der Zulieferung steht; `summary` + (Markenübersicht) fasst die Einleitung zusammen; `highlights` sind drei Stichzeilen aus den + Leistungen. Englisch: eigene Übersetzung in gleicher Struktur. +- `widget_label` „Persönliche Beratung zur Maßarbeit vereinbaren“ bleibt. + +### Bilder + +13 Dateien, konvertiert wie in Nachtrag 16 (WebP, max. 1920 px, unter +`public/img/assets/marken/otterpohl/`, zusammen rund 2,4 MB; ein Bild per GD nachverdichtet). + +- **Titelbild** `werkstatt-titel.webp`: zwei Mitarbeiter in der Werkstatt. +- **Aus der Werkstatt** (2–5): CNC-Bearbeitungszentrum, Türeinbau vor Ort, Handwerkzeug, + Anlieferung per Möbellift. Dafür gibt es eine **dritte Galerie-Kategorie** `company` + (`CmsBrand::GALLERY_COMPANY`, Badge „Aus der Werkstatt“). Im Admin: Button „Werkstatt“ und + Option „Aus der Werkstatt“ in der Kategorie-Auswahl, Hinweistext ergänzt. +- **Referenzen** (8, Kategorie `project`, Badge „Realisiertes Projekt“): Treppen, Lamellenwand mit + Sitzbank, Tür und Kommode, Wandregale, Objektbau-Verkaufsraum, Büro-Sideboard. Unterschriften + beschreiben das Motiv; die Kundennamen aus den Dateinamen (Lücke, Berkemeier, Neugebauer, + Dziuballe) stehen bewusst nicht auf der Seite. ⚠️ `Otterpohl_11.jpg` (Sideboard im Büro) ist + nach Kevins Angabe eine Referenz, nicht Werkstatt – falls es das eigene Büro ist, im Admin auf + „Aus der Werkstatt“ umstellen. +- **Reihenfolge in der Galerie:** Projekte, dann Werkstatt, dann Visualisierungen. +- **Überschrift der Galerie** richtet sich jetzt nach dem Inhalt: nur Visualisierungen → + „Inspirationen und Entwürfe“ (wie bisher, CABINET/Rotpunkt/Febrü); nur Projekte → „Referenzen“; + Projekte und Werkstatt → „Referenzen und Einblicke“ (Otterpohl); mit Visualisierungen → + „Projekte und Entwürfe“. Die Zeile darunter passt jeweils. +- ⚠️ **Referenzen-Seite:** `/referenzen` zeigt, solange im Modul „Referenzen“ nichts gepflegt ist, + die Projekte aus den Markengalerien – jetzt also die acht Otterpohl-Referenzen mit Link auf die + Markenseite. Werkstattbilder erscheinen dort nicht. Wenn Marcel das nicht will: Bilder im Admin + auf „Aus der Werkstatt“ stellen oder eigene Referenzen anlegen, dann greift der Fallback nicht. + +**Lokal ausgeführt:** `php artisan db:seed --class=OtterpohlBrandSeeder`, `php artisan db:seed +--class=BrandImageSeeder`. Beim Deployment: Abschnitt 8.2, Schritte 11 und 12 (Texte +unverändert, sie decken v2 mit ab). + +**Tests:** `FourthBrandTest` (v2-Überschriften, acht Leistungen, drei Abschnitte, Schlussblock hinter +den Leistungen, kein „Seit 2014“), `BrandFeedbackTest` (Seeder setzt Name, acht Leistungen, +Abschluss), `BrandImageSeederTest` (+3: Otterpohl-Galerie 8 Projekte + 4 Werkstatt ohne +Kundennamen, Badges und Überschrift „Referenzen und Einblicke“, Referenzen-Fallback zeigt Projekte +und keine Werkstattbilder), `BrandPageTest` (gemischte Galerie → „Projekte und Entwürfe“, Aufräum- +Seeder lässt Otterpohl 12). + +**Offen:** Marcel prüft Unterschriften, die Zuordnung von `Otterpohl_11`, das gestrichene „seit +2014“ und ob die Otterpohl-Referenzen unter `/referenzen` erscheinen sollen. + +## Nachtrag 19 · Entwurf der Startseite in der Farbwelt „Greige“ (24.09.2026) + +Briefing von Marcel (`dev/2026-08-28/farbwelt-kunde-24-09-2026.md`, Visualisierung +`Farbwelt und Stil.png`): die Creme-Palette wird nicht weiterverfolgt, die Richtung „Neu“ soll +heller, klarer und hochwertiger werden – heller, leicht kühler Grundton (Greige, kein steriles +Weiß), deutlich präsenteres Tannengrün, Gold nur als feine Linie. Dazu eine Slideshow im Hero, +neue Überschrift, keine Aktionsleiste, ein Button im Hero, neuer Projekttext, Logo mit Goldbogen. +Kevins Auftrag: eine **separate Version der Startseite** zum Vergleich, das Logo als Kopie. + +### Was gebaut ist + +- **Entwurfsseite `/entwurf`** (Route `home.draft`, View `web/home-draft.blade.php`): die reguläre + Startseite mit neuem Hero, ohne Ankündigungsleiste, Body-Klasse `palette-greige`, `noindex`. + Alle übrigen Sektionen sind unverändert eingebunden und färben sich über die Tokens. Die Seite + hängt am Farb-Schalter: mit `B2IN_PALETTE_SWITCH=false` liefert sie 404. +- **Palette „Greige“** als drittes Token-Set in `theme-b2in.css` (`.world-interior.palette-greige`, + am Body, nicht am `` wie der Browser-Schalter – so überlagern sich beide nicht): + + | Token | Wert | + |---|---| + | Grund | `#f0eeeb` Greige, Off-White (Kopfzeile der Visualisierung) | + | Karten / Hero-Bühne | `#f8f7f4` | + | Sektionswechsel (`bg-accent`) | `#e8e6e0` | + | Tannengrün (`--primary`) | `#14342c`, Buttons, dunkle Sektionen, Footer | + | Gold (`--secondary`) | `#aa8341` gedeckt; als Text `--secondary-deep` (dunkler) | + | Text | Tannengrau `hsl(166 25% 13%)` | + | Footer | Tannengrün mit Greige-Text, Goldlinie oben, negatives Logo | + +- **Logo-Kopien** `public/img/logos/b2in-interior-logo-gold-{positive,negative}.svg`: Bogen + `#AA8341` statt Ocker, Wortmarke im neuen Tannengrün `#14342C` (negativ: Schrift `#F0EEEB`). + Flächig, ohne Verlauf – die Verläufe liefert Marcel nach; die Dateien lassen sich dann 1:1 + ersetzen. Die bisherigen Logos sind unangetastet. `ThemeHelper::getLogoPath()` liefert die + Gold-Variante, sobald `b2in_palette()` (neuer Helper, Binding `b2in.palette`) „greige“ meldet + – das setzt nur die Entwurfsroute. Kein PNG erzeugt (kein SVG-Rasterizer im Container). +- **Hero mit Slideshow** (`HeroSlider`, Sektion `home/hero_slider`, im Seitenbaum unter Startseite + mit Vorschau auf `/entwurf`): Rahmen mit 1-px-Goldkante und Schatten, tannengrüne Flanken rechts + (volle Höhe) und links unten wie in der Visualisierung, Eyebrow „B2IN INTERIOR ——“, Überschrift + „*Räume aus einer Hand.* Mit starken Marken und persönlichen Lösungen.“ (erste Zeile in Gold, + Serife), Einleitungstext aus der Visualisierung, **ein** Button „Unsere Marken entdecken“ mit + Goldkante unten und Pfeil. Kein zweiter Button (Briefing Punkt 5), das Termin-Widget bleibt. + - Bild rechts im festen Seitenverhältnis 4:3 → auf großen Bildschirmen nicht mehr unten + beschnitten (Briefing Punkt 2). + - Slides: Liste `slides` der Sektion (je `image`, `label`, `caption`, `active`), solange leer + die **Titelbilder der veröffentlichten Kernmarken** in ihrer Reihenfolge (aktuell CABINET, + Rotpunkt, Febrü, Otterpohl sofern Bild). Kleine Marken-Pille unten links im Bild. + - Ruhig: Überblendung 1,4 s, Wechsel alle 7 s (`interval`, begrenzt auf 3–30 s), Pause bei + Hover/Fokus, Pfeiltasten, Punkte unter dem Rahmen als Tabs; bei „reduzierter Bewegung“ kein + Autowechsel. Läuft ohne eigenes JS-Bundle (Alpine im Markup). +- **Farb-Schalter** hat jetzt drei Einträge: Bisher / Neu (Browser, wie gehabt) / **Entwurf** + (Link auf `/entwurf`). Auf dem Entwurf ist „Entwurf“ aktiv, „Bisher“/„Neu“ führen mit + `?palette=…` zurück auf die Startseite. +- **Projekttext** (Briefing Punkt 7) global: Lang de/en geändert, `AudienceSwitchProjectTextSeeder` + tauscht den Satz in einem vorhandenen CMS-Eintrag (nur diesen Satz, idempotent). Lokal + ausgeführt (2 Stellen). Deployment Schritt 15. + +**Nachbesserung nach Kevins Screenshot (24.09., 17:30):** Die Goldkante war auf Greige nicht zu +sehen – jetzt ein Verlauf (hell oben links, auslaufend, in der Ecke wieder anziehend) über einen +maskierten Pseudo-Rand von 1,5 px. Die Flanken sind gerundet (Radius 2,75 rem rechts, 2 rem +links) und laufen hinter das Bild; ihre Goldlinie folgt der Rundung. Das Bild füllt den Rahmen +fast ganz (5:4, nur 1,25 rem Luft oben und unten, rechts über der Flanke), die Überschrift ist +kleiner (max. 3,3 rem statt 4,1 rem) und der Einleitungstext eine Stufe kleiner. + +**Zweite Nachbesserung (25.09.):** Hinter der Goldkante schien eine helle Linie durch – die +Masken-Technik ließ die gerundete Kante des Rahmens außerhalb des Rings sichtbar. Jetzt sind +Rahmen und Bild-Rahmen echte Verlaufs-Borders (transparenter Rand, Fläche auf `padding-box`, +Goldverlauf auf `border-box`), ohne Pseudo-Element. Der Button trägt den Goldrand rundum, unten +3 px, der Pfeil ist Gold. Die grünen Flanken sind **Inline-SVGs** +(`components/web/hero-flank-right|left.blade.php`): grüner Verlauf, Goldlinie entlang der +Rundung, rechts zusätzlich die kurze Zierlinie aus der Visualisierung. Sie skalieren über die +Rahmenhöhe (`height: 100 %` bzw. `46 %`, Breite aus dem viewBox-Verhältnis, Strichstärke bleibt +1,5 px dank `vector-effect`), Farben aus den Tokens. Liefert Marcel eigene SVGs, werden die +beiden Dateien getauscht – der Platz im Rahmen bleibt. + +**Abstimmung 25.09. (Kevin):** Für die nächste Runde ist der Farb-Schalter **aus** und die Seite +steht fest auf „Neu“. Dafür zwei neue Konfigurationswerte in `config/app.php`: +`B2IN_PALETTE_SWITCH` (Standard jetzt `false`), `B2IN_PALETTE` (Standard `modern`; ohne Schalter +setzt das Layout die Klasse `palette-modern` serverseitig an ``, mit Schalter ist es die +Vorbelegung, solange nichts im localStorage liegt) und `B2IN_HOME_DRAFT` (Standard `true`) – der +Entwurf `/entwurf` hängt nicht mehr am Schalter, sondern an diesem Wert. Der Schalter selbst +bleibt im Code und lässt sich mit `B2IN_PALETTE_SWITCH=true` wieder einblenden. + +### Was aus dem Briefing noch offen ist + +| Punkt | Stand | +|---|---| +| 1 Slideshow selbst pflegen (Upload, Reihenfolge, an/aus) | Datenmodell steht (Liste `slides` mit `active`), Pflege bisher nur über den JSON-Editor unter Inhalte. Eigene Oberfläche wie bei den Displays: nächster Schritt nach Freigabe des Entwurfs. | +| 6 Persönliches Foto | Kein Foto geliefert. Vorschlag: in der Gründer-Leiste („B2in by Marcel Scheibe“), Bildfeld ist dort schon vorhanden (`founder_bar.image`). | +| 8 Feinabstimmung der Töne | Werte oben sind aus der Visualisierung gesampelt und leicht aufgehellt („noch etwas heller“). Marcel entscheidet am Entwurf. | +| 9 Logo mit Verläufen | Kommt von Marcel; bis dahin flache Gold-Kopie. | +| Entwurf → Standard | Wenn freigegeben: Tokens von `.palette-greige` nach `:root`, Gold-Logos umbenennen, `home.blade.php` auf `hero-slider` umstellen, Ankündigungsleiste global aus, Schalter und `/entwurf` abschalten. Ein Nachtrag, kein Umbau. | + +**Lokal ausgeführt:** `php artisan db:seed --class=AudienceSwitchProjectTextSeeder`, `npm run build:b2in`. +Beim Deployment: Abschnitt 8.2, Schritt 15, dazu `npm run build:web`; die SVG-Logos liegen im +Repository. Link für Marcel: `https:///entwurf`. + +**Tests:** `HomeDraftTest` (16: Palette und Gold-Logos nur auf dem Entwurf, Überschrift/ein +Button/keine Leiste, Sektionsreihenfolge, 404 ohne Schalter, Slides aus Marken, ohne Bild, +CMS-Slides mit `active`, Intervall-Grenzen, Schalter hin und zurück, Logo-Dateien, Tokens, +Projekttext in Lang und per Seeder), `CmsPageTreeTest` (Reihenfolge um `hero_slider` ergänzt). +Gesamt: 1049 grün. + diff --git a/dev/2026-08-28/farbwelt-kunde-24-09-2026.md b/dev/2026-08-28/farbwelt-kunde-24-09-2026.md new file mode 100644 index 00000000..986a4859 --- /dev/null +++ b/dev/2026-08-28/farbwelt-kunde-24-09-2026.md @@ -0,0 +1,69 @@ +1. Hero-Bereich / Slideshow +Das aktuelle große Hero-Bild würde ich gerne durch eine Slideshow mit unterschiedlichen Herstellern und Themen ersetzen, zum Beispiel: +Rotpunkt / Küche +CABINET +Febrü +Tischlerei / individueller Innenausbau +komplette Raum- und Einrichtungskonzepte +Wichtig wäre mir, dass ich diese Slideshow selbstständig pflegen und steuern kann, ähnlich wie unsere Displays. +Ideal wäre eine Möglichkeit im Backend, mit der ich: +Bilder selbst hochladen kann +die Reihenfolge verändern kann +einzelne Slides aktivieren oder deaktivieren kann +Bilder später unkompliziert austauschen kann +Die Slideshow selbst sollte dabei ruhig und hochwertig wirken – also langsame, dezente Übergänge und kein klassischer Werbebanner-Effekt. + +2. Darstellung und Größe des Hero-Bildes +Wichtig wäre mir außerdem, dass das Hero-Bild auf größeren Bildschirmen nicht unten abgeschnitten wird. +Auf meinem Monitor – ich meine, es sind 27 Zoll – wird das aktuelle Motiv unten sichtbar beschnitten. Dadurch wirkt der Bildausschnitt aus meiner Sicht nicht ganz stimmig. +Bei der mitgeschickten Visualisierung ist das wesentlich besser gelöst: Das Motiv ist nahezu vollständig sichtbar und bekommt dadurch deutlich mehr Wirkung. +So würde ich es gerne haben – natürlich responsive für unterschiedliche Bildschirmgrößen. + +3. Headline +Die bisherige Headline würde ich gerne ändern zu: +„Räume aus einer Hand. Mit starken Marken und persönlichen Lösungen.“ +Die Formulierung B2in Interior kann dabei gerne, ähnlich wie in der Visualisierung, etwas zurückhaltender oberhalb bzw. im Umfeld der Headline eingesetzt werden. + +4. Obere Aktionsleiste +Die derzeitige Aktionsleiste ganz oben würde ich komplett entfernen. +Ohne diese Leiste wirkt die Seite aus meiner Sicht ruhiger, klarer und insgesamt hochwertiger. + +5. Buttons im Hero +Den zusätzlichen Button „Termin anfragen“ neben „Unsere Marken entdecken“ würde ich entfernen. +Der feststehende Button „Termin anfragen“ rechts unten soll dagegen bestehen bleiben. +Damit gibt es im eigentlichen Hero einen klaren Hauptbutton: +„Unsere Marken entdecken“ + +6. Persönliches Foto +Ein Foto von mir würde ich gerne dezent und hochwertig auf der Seite integrieren. +Nicht im Hero und auch nicht zu dominant, sondern an einer passenden Stelle weiter unten – zum Beispiel in Verbindung mit dem persönlichen B2in-/Marcel-Scheibe-Bereich. + +7. Text Projektgeschäft +Die bisherige Formulierung mit „deutscher Vertragssicherheit“ würde ich gerne ersetzen durch: +„Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.“ + +8. Farbwelt und Stil +Die bisherige Farbwelt würde ich nicht weiterverfolgen. Die neue Richtung gefällt mir deutlich besser, ich würde sie allerdings noch etwas heller, klarer und hochwertiger gestalten. +Das beigefügte Bild zeigt die Richtung aus meiner Sicht schon sehr gut. +Besonders wichtig wären mir dabei: +ein heller, leicht kühler Grundton +kein reines bzw. steriles Weiß +eher ein sehr helles, modernes Greige / Off-White +ein deutlich präsenteres und klareres Tannengrün +Gold nur als feiner Akzent +Gerade die Kombination aus Tannengrün und einer sehr dünnen Goldlinie gefällt mir gut. +Im beigefügten Beispiel sieht man das unter anderem: +an der feinen goldenen Umrandung des Hero-Bereichs +in Verbindung mit den tannengrünen Flächen +am Button „Unsere Marken entdecken“, der ebenfalls eine feine Goldkante bzw. einen Goldakzent erhält +bei kleinen Linien und Details +Genau so würde ich Gold einsetzen: nicht flächig und nicht dekorativ-verspielt, sondern sehr reduziert als hochwertiges Detail. +Die Kombination aus hellem Untergrund + kräftigem Tannengrün + sehr feinem Gold trifft für mich die gewünschte Richtung inzwischen ziemlich gut. + +9. B2in-Logo an die neue Farbwelt anpassen +Wenn wir die Farbwelt entsprechend weiterentwickeln, sollte sich das aus meiner Sicht konsequenterweise auch im B2in-Logo widerspiegeln. +Der Bogen über dem B2in-Schriftzug ist aktuell eher in einem Ocker-/Senfton gehalten. Diesen würde ich gerne auf den neuen Goldton umstellen, den wir auch für die feinen Linien und Akzente auf der Website verwenden. +Damit würde sich die Farbsprache durchgängig wiederfinden: +Tannengrün + Gold + heller, neutraler Grundton. +Ich würde dich daher bitten, die vorhandenen Logo-Varianten entsprechend anzupassen, sodass der bisherige Ockerton konsequent durch den neuen Goldton ersetzt wird und Website und Logo anschließend exakt dieselbe Farbwelt verwenden. +Wichtig wäre mir auch hier, dass das Gold edel und zurückhaltend bleibt – also kein gelblicher oder zu glänzender Goldton, sondern eher ein moderner, gedeckter Premium-Goldton wie in der beigefügten Visualisierung. \ No newline at end of file diff --git a/dev/2026-08-28/feedback-17-09-2026.md b/dev/2026-08-28/feedback-17-09-2026.md new file mode 100644 index 00000000..d6521381 --- /dev/null +++ b/dev/2026-08-28/feedback-17-09-2026.md @@ -0,0 +1,83 @@ +#1 + +Die Richtung passt deutlich besser: locker, verständlich und ohne typischen Werbeprospekt-Ton. + +Besonders gut finde ich den Einstieg „Für den Anfang reicht uns ein Bild und ein grobes Maß völlig aus.“ Der nimmt sofort eine Hürde. Auch „Sie müssen also nicht erst alles millimetergenau ausmessen“ spricht genau die Frage an, die ein Kunde wahrscheinlich im Kopf hat. + +Nur zwei Stellen würde ich noch ändern: „valides Angebot“ klingt im ansonsten lockeren Text etwas technisch. Ich würde „ein wirklich brauchbares Angebot“ oder „ein konkretes Angebot“ schreiben. Und statt „zu Ihnen nach Hause“ lieber „bei Ihnen vor Ort“, weil ihr ja nicht ausschließlich Privatwohnungen plant. + +Dann wäre meine Fassung: + +Für den Anfang reicht uns ein Bild und ein grobes Maß völlig aus. + +Sie müssen also nicht erst alles millimetergenau ausmessen. Ein paar Fotos von der Raumsituation und die ungefähren Maße reichen, damit wir schon eine gute Planung machen und Ihnen ein konkretes Angebot erstellen können. + +Danach gehen wir gemeinsam ins Detail: Aufteilung, Ausstattung, Materialien, Farben und natürlich der Preis. + +Wenn alles grundsätzlich passt, kommen wir zum professionellen Aufmaß bei Ihnen vor Ort. Danach machen wir die Feinplanung fertig und geben den Schrank zur Bestellung frei. + +So läuft’s: + +Foto + grobe Maße → Beratung → Planung & Angebot → Aufmaß vor Ort → Feinplanung → Bestellung + +Das wäre für mich ziemlich genau die richtige Mischung aus persönlich, unkompliziert und trotzdem hochwertig. + + + +#2 Febrü + +Ja. Ich würde wirklich nur die vereinbarten Stellen ändern und den Rest der Seite lassen. Die aktuelle Seite hat diese Passagen an den entsprechenden Stellen. + +Bei uns gibt es Kunden – keine Schubladen + +Ob Privatkunde oder Unternehmen: Für uns gibt es zunächst einmal Kunden mit unterschiedlichen Räumen, Anforderungen und Stückzahlen. + +Natürlich beeinflusst die Menge den Preis. Fünfzig Arbeitsplätze werden anders kalkuliert als ein einzelner Schreibtisch fürs Homeoffice. Der Anspruch an Produkt, Beratung und Service bleibt trotzdem derselbe. + +Febrü bleibt Febrü. B2in bleibt B2in. Und jeder Kunde erhält eine Lösung, die zu seinem tatsächlichen Bedarf passt. + +Ob im Kleinen oder im Großen – die Detailtreue entscheidet. + +Aus Herford. Für moderne Arbeitswelten. + +Febrü entwickelt und produziert hochwertige Büromöbel direkt im ostwestfälischen Herford – nur wenige Kilometer von Bielefeld entfernt. Eine sehr hohe Fertigungstiefe ermöglicht kurze Wege, verlässliche Qualität und hohe Flexibilität bei Materialien, Farben und individuellen Anforderungen. + +Das ist auch sachlich sauber: Febrü selbst spricht aktuell von einer hohen Fertigungstiefe und nennt auf der Unternehmensseite weiterhin 95 %. Mit „sehr hohe Fertigungstiefe“ seid ihr also auf der sicheren Seite, ohne euch an eine konkrete Zahl zu binden. + +Wertschätzung ist keine Frage der Stückzahl + +Gerade in Zeiten des Fachkräftemangels können Unternehmen es sich nicht leisten, Arbeitsbedingungen als Nebensache zu behandeln. Wer Top-Leistung erwartet, muss die passenden Voraussetzungen schaffen. Ein Obstkorb und kostenloses Wasser sind freundlich – ersetzen aber keinen guten Arbeitsplatz. + +Besonders deutlich wird das bei Menschen, die Beruf und Familie miteinander organisieren. Teilzeit bedeutet nicht halbe Leistung. Oft wird in einem kürzeren Zeitfenster hoch konzentriert gearbeitet, während gleichzeitig Schule, Termine, Betreuung und der ganz normale Familienalltag organisiert werden müssen. + +Gerade hier darf der Arbeitsplatz kein Provisorium aus Küchentisch, Beistelltisch oder Schlafzimmerecke sein. Er muss sofort funktionieren, Bewegung ermöglichen, technisch durchdacht sein und sich trotzdem vernünftig in das Zuhause einfügen. Für den Menschen, der ihn täglich nutzt, ist ein einzelner Arbeitsplatz keine kleine Lösung. Er ist seine vollständige Arbeitswelt. + +Unverändert lassen + +„Nicht drei Marken. Ein durchgängiger Raum.“ würde ich komplett so stehen lassen. Das Zusammenspiel von Febrü, Rotpunkt, CABINET und B2in ist dort sehr gut erklärt. + +„Das Wir und das Ich“ ebenfalls unverändert lassen. Gerade der Einstieg „Ja, ja – das könnte man unter dem üblichen Verkaufs-Blabla abheften“ gibt der Seite Persönlichkeit und passt zum restlichen Ton. + +Beim doppelten Satz „Ob im Kleinen oder im Großen – die Detailtreue entscheidet.“ würde ich die erste Version oben stehen lassen und die zweite Version nach „Erleben statt nur aus dem Katalog auswählen“ löschen. Aktuell steht er tatsächlich zweimal auf der Seite. + +Den mitlaufenden CTA würde ich ebenfalls nicht verändern. + + +#3 Rotpunk + +#a text erstezen "Mehr als Küchen" +Mehr als Küchen + +Die Möglichkeiten von Rotpunkt enden nicht bei der klassischen Küche. Das Möbel- und Planungssystem lässt sich auch für Hauswirtschaftsräume, Badezimmer, Sideboards, Wohnmöbel und weitere Bereiche rund um das Wohnen einsetzen. + +Materialien, Farben und Gestaltungselemente können dabei über mehrere Räume hinweg aufgegriffen werden. So entsteht ein stimmiges Gesamtbild, statt in jedem Raum wieder ganz von vorne anzufangen. + +Bei Bedarf denken wir den Innenraum deshalb auch über die Küche hinaus – von Möbeln und Beleuchtung bis zu Tapeten, Fußböden und, wenn es sein soll, der letzten Blume. + +#b Thema Handwerk +Das Thema Handwerk am Ende von Rotpunkt sollte so formuliert sein, dass es bei jeder Marke als letzte Ergänzung angezeigt wird + +#3 Cabinet +"Die Fragen die wir vorher stellen" +zweiter Absatz ersetzen: +Auch die Dinge, die man nicht jeden Tag braucht, gehören in die Planung: Wäschekörbe, Bügelbrett, Staubsauger, Karnevalskostüme oder besondere Sportsachen. Wenn wir vorher wissen, was untergebracht werden soll, können wir den Platz direkt sinnvoll dafür einplanen. \ No newline at end of file diff --git a/dev/2026-08-28/logos/B2In-Logo-negativ-2025-09-22_RGB.svg b/dev/2026-08-28/logos/B2In-Logo-negativ-2025-09-22_RGB.svg new file mode 100644 index 00000000..0f6335a4 --- /dev/null +++ b/dev/2026-08-28/logos/B2In-Logo-negativ-2025-09-22_RGB.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev/2026-08-28/logos/B2In-Logo-positiv-2025-09-22_RGB.svg b/dev/2026-08-28/logos/B2In-Logo-positiv-2025-09-22_RGB.svg new file mode 100644 index 00000000..6e1e1565 --- /dev/null +++ b/dev/2026-08-28/logos/B2In-Logo-positiv-2025-09-22_RGB.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev/2026-08-28/logos/B2In_Interior-Logo-negativ-2026-09-07_RGB.svg b/dev/2026-08-28/logos/B2In_Interior-Logo-negativ-2026-09-07_RGB.svg new file mode 100644 index 00000000..d7bcd586 --- /dev/null +++ b/dev/2026-08-28/logos/B2In_Interior-Logo-negativ-2026-09-07_RGB.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/2026-08-28/logos/B2In_Interior-Logo-positiv-2026-09-07_RGB.svg b/dev/2026-08-28/logos/B2In_Interior-Logo-positiv-2026-09-07_RGB.svg new file mode 100644 index 00000000..dbead3fa --- /dev/null +++ b/dev/2026-08-28/logos/B2In_Interior-Logo-positiv-2026-09-07_RGB.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/2026-08-28/markenlogos/cabinet_logo_2026.svg b/dev/2026-08-28/markenlogos/cabinet_logo_2026.svg new file mode 100644 index 00000000..7957e0b8 --- /dev/null +++ b/dev/2026-08-28/markenlogos/cabinet_logo_2026.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/dev/2026-08-28/markenlogos/februe_Logo_2022.png b/dev/2026-08-28/markenlogos/februe_Logo_2022.png new file mode 100644 index 00000000..5dfc33d3 Binary files /dev/null and b/dev/2026-08-28/markenlogos/februe_Logo_2022.png differ diff --git a/dev/2026-08-28/markenlogos/otterpohl.jpg b/dev/2026-08-28/markenlogos/otterpohl.jpg new file mode 100644 index 00000000..31bb7d18 Binary files /dev/null and b/dev/2026-08-28/markenlogos/otterpohl.jpg differ diff --git a/dev/2026-08-28/markenlogos/rotpunkt_logo_2020.svg b/dev/2026-08-28/markenlogos/rotpunkt_logo_2020.svg new file mode 100644 index 00000000..bb84b63f --- /dev/null +++ b/dev/2026-08-28/markenlogos/rotpunkt_logo_2020.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/2026-08-28/otterpohl_Webseitentext_FINAL.md b/dev/2026-08-28/otterpohl_Webseitentext_FINAL.md new file mode 100644 index 00000000..1fcc83be --- /dev/null +++ b/dev/2026-08-28/otterpohl_Webseitentext_FINAL.md @@ -0,0 +1,131 @@ +Otterpohl – nicht „easy to click & buy“, aber trotzdem easy to get +Vieles kann man heute online finden, konfigurieren und direkt bestellen. Und das ist auch gut so. +Aber manchmal möchte man etwas Besonderes. Eine eigene Idee. Ein Möbel, das genau so aussehen soll, wie man es im Kopf hat. Ein ungewöhnliches Detail. Eine besondere Kombination aus Materialien. Oder einfach etwas, das man so auf keiner Webseite und auch nicht im Handel findet. +Und manchmal beginnt es nur mit einem Foto und dem Satz: +„So ungefähr – aber eben für mich.“ +Das ist vielleicht nicht „easy to click & buy“. +Aber es darf trotzdem easy to get sein. +Denn genau dafür sind wir da: Sie bringen die Idee mit – und wir kümmern uns gemeinsam mit Otterpohl um Planung, handwerkliche Umsetzung und Montage. +Mit unseren Möbelpartnern können wir bereits sehr individuell planen. Mit Otterpohl kommen zusätzlich Tischlerhandwerk, individuelle Fertigung, besondere Oberflächen und Innenausbau dazu. +Und genau diese Kombination erweitert unsere Möglichkeiten noch einmal erheblich. +Ziemlich nah an der eierlegenden Wollmilchsau +Der Begriff ist vielleicht nicht besonders elegant. +Aber ganz falsch ist er nicht. +Individuelle Möbel und Sonderanfertigungen. Lackierungen nach Wunsch. Echtholzfurniere. Verkleidungen. Ergänzungen. Anpassungen. Innenausbau. Trockenbauwände. Individuelle Lösungen direkt vor Ort. +Und ganz wichtig: +inklusive Montage. +Damit endet die Arbeit eben nicht am Möbel. +Wenn für eine gute Lösung noch eine Trockenbauwand entstehen muss, Technik verschwinden soll, eine Verkleidung gebraucht wird oder verschiedene Elemente sauber miteinander verbunden werden müssen, kann auch das Teil des Projekts sein. +Für uns ist genau das der große Vorteil: +Planung, Tischlerhandwerk, Innenausbau und Montage greifen ineinander. +Nicht erst das Möbel planen, danach einen Trockenbauer suchen, anschließend jemanden für die Anpassung und am Ende noch ein Montageteam koordinieren. +Sondern möglichst alles aus einer Hand – und am Ende auch optisch aus einem Guss. +Seit 2014 an unserer Seite +Otterpohl ist für uns kein Partner, den wir irgendwann für diese Webseite entdeckt haben. +Wir arbeiten bereits seit 2014 bei der Montage zusammen. +Und das ist für uns wesentlich mehr als eine kleine Fußnote. +Denn ein hochwertiges Möbel ist am Ende nur so gut wie seine Montage. +Saubere Fugen. Präzise Anschlüsse. Passleisten, die tatsächlich zur Wand passen. Fronten, die vernünftig eingestellt sind. Und Menschen vor Ort, die eine Situation nicht nur montieren, sondern auch handwerklich verstehen. +Gerade wenn Möbelbau, Tischlerarbeiten, Trockenbau und Innenausbau zusammenkommen, ist diese Erfahrung besonders wertvoll. +Nach so vielen Jahren weiß man ziemlich genau, mit wem man arbeitet. +Und bei einem Partner, den wir zu unseren Kunden schicken, finden wir das durchaus wichtig. +Farbe darf auch genau die Farbe sein +Bei manchen Projekten reichen drei ähnliche Farbtöne eben nicht. +Vielleicht soll ein Möbel exakt zu einer vorhandenen Wandfarbe passen. Zu einem anderen Möbel. Zu einem bestimmten Material. Oder Sie haben einfach diesen einen Farbton im Kopf und möchten genau den. +Dann ist „fast passend“ manchmal eben nicht passend genug. +Mit individuellen Lackierungen entstehen Möglichkeiten, die weit über eine normale Farbpalette hinausgehen. +Und genauso ist es bei den Materialien. +Manchmal ist eine hochwertige Dekoroberfläche genau richtig. +Manchmal soll es Lack sein. +Und manchmal möchte man eben echtes Holz – mit echter Maserung, echter Struktur und einem Echtholzfurnier, bei dem kein Möbelstück exakt aussieht wie das andere. +Das ist dann eben der echte Charakter eines Möbels. Nicht perfekt gleich – sondern genau deshalb besonders. +Manchmal gibt es die Idee schon – nur das Möbel noch nicht +Vielleicht haben Sie eine ziemlich genaue Vorstellung. +Vielleicht nur ein Foto, eine Skizze oder eine Idee, die Sie irgendwo gesehen haben. +Das reicht völlig. +Dann überlegen wir gemeinsam: Wie könnte man das umsetzen? Welche Proportionen passen? Welche Materialien? Lack oder Furnier? Welche Funktion soll das Möbel übernehmen? Und wie fügt sich alles in den Raum und in das ein, was bereits vorhanden ist? +Nicht jede gute Idee beginnt schließlich mit einer Artikelnummer. +Manchmal beginnt sie einfach im Kopf. +Aus der Idee wird eine Planung. +Aus der Planung ein Möbel. +Und irgendwann steht es tatsächlich da. +Nicht unbedingt easy to click & buy – aber am Ende trotzdem ziemlich easy to get. +Individuelle Planung kann viele Formen haben +Wir arbeiten bewusst mit unterschiedlichen Herstellern und Partnern. +CABINET, Rotpunkt und Febrü bieten bereits enorme Möglichkeiten, Möbel individuell zu planen. Maße, Farben, Materialien, Ausstattung und Funktionen können auf den Raum und auf die Menschen abgestimmt werden, die ihn später nutzen. +Mit Otterpohl erweitern wir diese Möglichkeiten um die handwerkliche Fertigung. +Ein Möbel nach eigenem Entwurf. Eine besondere Konstruktion. Eine individuelle Verkleidung. Eine Bank. Ein Tisch. Ein Wandpaneel. Eine spezielle Lackierung. Ein Echtholzfurnier. Oder einfach dieses eine Detail, das sich handwerklich besser lösen lässt. +Tischlerhandwerk ist damit für uns keine Alternative zu unserer Planung. +Es ist eine zusätzliche Möglichkeit innerhalb unserer Planung. +Nicht alles muss individuell gebaut werden +Auch das gehört für uns zu einer vernünftigen Beratung. +Nur weil ein Tischler etwas bauen kann, muss man nicht automatisch jedes Möbel individuell fertigen lassen. +Wenn ein hochwertiges Möbelsystem eine Aufgabe perfekt löst, nutzen wir dieses System. +Wenn eine individuelle Ergänzung sinnvoll ist, wird genau diese Ergänzung gebaut. +Und wenn die beste Lösung tatsächlich eine komplett freie Anfertigung ist, geht eben auch das. +Wir glauben nicht besonders an entweder oder. +Wir glauben eher daran, für jede Aufgabe die sinnvollste Lösung zu finden. +Ein Raum hört nicht am Möbel auf +Ein Interior-Projekt hält sich erstaunlich selten sauber an einzelne Gewerke. +Da steht vielleicht irgendwann das perfekte Möbel – aber daneben fehlt noch eine Wand. Eine Nische soll entstehen. Technik muss verschwinden. Eine Verkleidung wird gebraucht. Ein Übergang soll so aussehen, als wäre er von Anfang an genau so geplant gewesen. +Dann hilft es wenig, wenn jeder nur bis zur Grenze seines eigenen Gewerks schaut. +Wir möchten lieber, dass am Ende alles zusammenpasst. +Möbel, Wand, Verkleidung, Oberfläche und Montage sollen nicht wie fünf einzelne Entscheidungen aussehen, sondern wie eine einzige Planung. +Oder einfacher: +Alles aus einem Guss. +Der letzte Zentimeter kann ziemlich wichtig sein +Tischlerhandwerk zeigt seine Stärke übrigens nicht nur bei spektakulären Einzelstücken. +Manchmal geht es um Dinge, die auf einer Visualisierung kaum auffallen. +Eine Wand läuft etwas weg. Ein Boden ist nicht vollkommen eben. Ein vorhandenes Bauteil muss integriert werden. Ein Übergang braucht eine saubere Lösung. +Auf dem Bildschirm sieht so etwas meistens ziemlich einfach aus. +In einem echten Raum manchmal weniger. +Genau dort entscheidet sich häufig, ob etwas am Ende nur irgendwie passt – oder ob es wirklich so aussieht, als wäre es schon immer genau so gedacht gewesen. +Unser Anspruch ist deshalb ziemlich einfach: +Man soll am Ende nicht sehen, wo es kompliziert war. +Gute Planung endet nicht am Bildschirm +Wir planen, visualisieren, bemustern und verändern. +Aber irgendwann muss jede Idee den Bildschirm verlassen. +Dann gibt es echte Wände, echte Böden, echtes Material und meistens mindestens eine Kleinigkeit, die auf keiner Zeichnung so richtig auffallen wollte. +Deshalb gehören für uns Planung und Handwerk zusammen. +Denn am Ende interessiert niemanden, welche technische Herausforderung wir irgendwo lösen mussten. +Es soll funktionieren. +Es soll gut aussehen. +Und es soll möglichst so wirken, als wäre es genau so immer gedacht gewesen. +Handwerk darf trotzdem modern sein +Beim Wort Tischlerei denken manche immer noch an dunkles Holz, schwere Möbel und eine Hobelbank. +Kann alles schön sein. +Ist aber nur ein kleiner Ausschnitt dessen, was heute möglich ist. +Modernes Tischlerhandwerk kann genauso geradlinig, reduziert und architektonisch sein. +Lackierte Oberflächen. Echtholzfurniere. Moderne Plattenwerkstoffe. Kombinationen verschiedener Materialien. Filigrane Konstruktionen oder ganz bewusst massive Elemente. +Am Ende entscheidet nicht die Vorstellung davon, wie ein „Tischlermöbel“ auszusehen hat. +Sondern die Idee dahinter. +Und genau das finden wir spannend. +Eine kleine Idee bleibt trotzdem eine gute Idee +Es muss übrigens nicht immer gleich der komplette Innenausbau sein. +Vielleicht fehlt nur ein Tisch. +Ein Regal. +Eine Bank. +Eine besondere Ablage. +Oder dieses eine Möbel, das man weder online noch im Handel so findet, wie man es eigentlich gerne hätte. +Dann sprechen wir darüber. +Nicht jede gute Idee muss automatisch ein großes Projekt werden. +Manchmal ist ein einziges besonderes Möbel genau das, was einem Raum noch gefehlt hat. +Aus Rheda-Wiedenbrück. Und ziemlich nah dran. +Otterpohl kommt aus Rheda-Wiedenbrück. +Das ist für uns nicht nur geografisch angenehm. +Gerade bei individuellen Projekten sind kurze Wege und direkte Kommunikation ziemlich wertvoll. +Man kennt sich. Man spricht miteinander. Man kann Situationen gemeinsam beurteilen und Lösungen entwickeln. +Und wenn etwas vor Ort anders aussieht als auf dem Papier, braucht es nicht erst fünf E-Mails und drei Weiterleitungen. +Dann braucht es jemanden, der sich die Sache anschaut und sagt: +„Okay. Kriegen wir hin.“ +Nach mehr als zehn Jahren gemeinsamer Projekte wissen wir ziemlich gut, was damit gemeint ist. +Planung trifft Handwerk +Am Ende geht es uns nicht darum, möglichst viele Marken und Partner auf unserer Webseite unterzubringen. +Es geht darum, möglichst viele gute Möglichkeiten zu haben. +Mit unseren Möbelpartnern können wir sehr individuell planen. +Mit Otterpohl kommen Tischlerhandwerk, freie Fertigung, individuelle Lackierungen, Echtholzfurniere, Innenausbau, Trockenbau und die handwerkliche Umsetzung inklusive Montage dazu. +Alles aus einer Hand. Und am Ende möglichst alles aus einem Guss. +Manchmal gibt es die passende Lösung schon. +Manchmal muss man sie planen. +Und manchmal muss man sie eben bauen. +Nicht immer easy to click & buy. Aber trotzdem easy to get. \ No newline at end of file diff --git a/dev/2026-08-28/otterpohl_Webseitentext_FINAL_v2.md b/dev/2026-08-28/otterpohl_Webseitentext_FINAL_v2.md new file mode 100644 index 00000000..d12d359a --- /dev/null +++ b/dev/2026-08-28/otterpohl_Webseitentext_FINAL_v2.md @@ -0,0 +1,41 @@ +Ziemlich nah an der eierlegenden Wollmilchsau +Der Begriff ist vielleicht nicht besonders elegant. Aber ganz falsch ist er nicht. +Individuelle Möbel. Türen. Treppen. Küchen. Wandverkleidungen. Objektbau. Sonderanfertigungen. Lackierungen und Echtholzfurniere. Bis hin zum kompletten Innenausbau. +Mit Otterpohl Möbelservice haben wir einen Partner an unserer Seite, bei dem die Möglichkeiten nicht am einzelnen Möbelstück aufhören. +Denn manchmal braucht ein Raum mehr als einen Schrank oder ein Sideboard. Vielleicht soll eine Treppe Teil des Gestaltungskonzepts werden. Eine Wand erhält eine individuelle Holzverkleidung. Eine Tür soll sich selbstverständlich in den Raum einfügen. Oder Möbel, Materialien und bauliche Elemente müssen so zusammenspielen, dass am Ende alles wie aus einem Guss wirkt. +Genau dort beginnt der individuelle Innenausbau. +Vom einzelnen Möbel bis zum ganzen Raum +Wir arbeiten mit Herstellern, die schon sehr viele individuelle Möglichkeiten bieten. CABINET, Rotpunkt und Febrü geben uns dafür eine starke Basis. +Aber es gibt Projekte, bei denen die Lösung darüber hinausgeht. +Mit Otterpohl Möbelservice können wir deshalb nicht nur individuelle Möbel und Sonderanfertigungen realisieren, sondern auch Türen, Treppen, Küchen, Wand- und Deckenverkleidungen sowie komplette Innenausbauten und Lösungen für den Objektbereich einbeziehen. +So können wir Räume ganzheitlicher denken – und Details nicht isoliert betrachten. +Planung trifft Handwerk +Manchmal entsteht die beste Lösung aus einem Möbelprogramm. Manchmal braucht es eine individuelle Ergänzung. Und manchmal muss etwas komplett neu gedacht und gebaut werden. +Genau diese Kombination macht für uns den Unterschied. +Wir planen den Raum und verbinden bei Bedarf unterschiedliche Hersteller, Materialien und handwerkliche Lösungen miteinander. Otterpohl Möbelservice ergänzt diese Planung um die Möglichkeiten einer klassischen Tischlerei und eines erfahrenen Innenausbauers. +Das Ergebnis kann ein einzelnes Möbel sein. +Oder ein kompletter Raum. +Leistungen und Möglichkeiten +Individuelle Möbel & Sonderanfertigungen +Möbel nach Maß, besondere Konstruktionen und individuelle Lösungen – nach Planung, Idee, Skizze oder konkreter Anforderung. +Innenausbau +Von einzelnen Ausbauarbeiten bis zum ganzheitlichen Raumkonzept – inklusive individueller Verkleidungen und maßgefertigter Einbauten. +Türen & Treppen +Individuelle Türen, Treppen und konstruktive Lösungen, die sich in Architektur und Einrichtung einfügen. +Küchen +Individuelle Küchenlösungen und handwerkliche Ergänzungen auch dort, wo klassische Küchenprogramme an ihre Grenzen kommen. +Wandverkleidungen & besondere Details +Holzverkleidungen, Lamellen, Paneele und individuelle Gestaltungselemente, die Architektur und Einrichtung miteinander verbinden. +Objektbau +Individuelle Lösungen für Büros, Verkaufsräume und gewerblich genutzte Flächen – vom Einzelmöbel bis zum kompletten Ausbau. +Lack & Echtholzfurnier +Individuelle Lackierungen und Furniere für Lösungen, bei denen Farbe, Material und Oberfläche exakt aufeinander abgestimmt werden sollen. +Montage +Die Umsetzung endet nicht bei der Fertigung. Entscheidend ist, dass Möbel, Ausbau und Raum am Ende auch vor Ort sauber zusammenkommen. +Das Ganze statt nur das Einzelne +Genau deshalb passt Otterpohl Möbelservice zu unserem Ansatz. +Wir möchten Räume nicht in einzelne Gewerke zerlegen. Wir möchten Lösungen schaffen, bei denen Möbel, Materialien, Architektur und handwerkliche Details zusammenpassen. +Mal braucht es dafür einen Hersteller. +Mal einen Tischler. +Und manchmal beides. +Nicht easy click & buy. Aber dennoch easy to get. \ No newline at end of file diff --git a/dev/2026-08-28/preview-deployment.md b/dev/2026-08-28/preview-deployment.md new file mode 100644 index 00000000..9e655c70 --- /dev/null +++ b/dev/2026-08-28/preview-deployment.md @@ -0,0 +1,343 @@ +# Vorschau-Instanz auf dem Live-Server einrichten + +**Ziel:** `https://preview.b2in.eu` zeigt den neuen Stand zur Abstimmung. **Die Live-Seite bleibt +unangetastet** – es wird keine einzige Datei im Live-Verzeichnis überschrieben und keine Zeile in +der Live-Datenbank geändert. + +**Prinzip:** zweites, eigenständiges Verzeichnis · eigene `.env` · eigene Datenbank. + +``` +/var/www/b2in/ ← Live, bleibt wie es ist +/var/www/b2in-preview/ ← Kopie davon, darauf das Update +``` + +> Pfade unten sind Beispiele. Ersetze `/var/www/b2in` durch das echte Live-Verzeichnis und +> `b2in_live` / `b2in_preview` durch die echten Datenbanknamen. + +--- + +## 0. Warum nicht einfach die Live-Dateien tauschen + +Zwei Gründe, die diesen Aufwand rechtfertigen: + +1. **Der Relaunch-Seeder überschreibt CMS-Sektionen.** `InteriorRelaunchCmsSeeder` schreibt 23 + Sektionen mit den neuen Texten. Auf der Live-Datenbank ausgeführt wäre der alte Stand weg. +2. **Migrationen sind nicht umkehrbar.** `cms_brands` und die beiden Spalten `summary` / + `highlights` kommen neu dazu. + +Mit einer eigenen Datenbank kann beim Testen nichts passieren, was die Live-Seite betrifft. + +--- + +## 1. Voraussetzungen + +- [ ] DNS-Eintrag `preview.b2in.eu` → dieselbe Server-IP wie `b2in.eu` +- [ ] SSL-Zertifikat für die Subdomain (Let's Encrypt genügt) +- [ ] vhost mit `DocumentRoot /var/www/b2in-preview/public` +- [ ] Zweite MySQL-Datenbank plus Benutzer +- [ ] Genug Plattenplatz für eine Kopie des Live-Verzeichnisses (vor allem `storage/`) + +--- + +## 2. Sicherung ziehen + +Auch wenn wir live nichts anfassen – der Dump ist zugleich die Grundlage für die Vorschau-DB. + +```bash +cd /var/www/b2in +mysqldump -u USER -p b2in_live > ~/b2in_live_$(date +%F_%H%M).sql +ls -lh ~/b2in_live_*.sql # Größe prüfen, darf nicht 0 sein +``` + +--- + +## 3. Vorschau-Verzeichnis anlegen + +Kopie des Live-Standes, **inklusive `vendor/`**. Das erspart ein `composer install` auf dem +Server – für `livewire/flux-pro` bräuchte es sonst die Zugangsdaten aus `auth.json`. + +```bash +cp -a /var/www/b2in /var/www/b2in-preview +cd /var/www/b2in-preview + +# Laufzeit-Reste der Kopie entfernen +rm -rf bootstrap/cache/*.php +rm -f public/storage # Symlink wird in Schritt 7 neu gesetzt +``` + +**Zu `storage/app/public` (Medienbibliothek, Display-Videos):** Durch `cp -a` liegt eine echte +Kopie in der Vorschau – sauber getrennt, kostet aber den Speicherplatz. Wenn das zu groß ist: + +```bash +rm -rf storage/app/public +ln -s /var/www/b2in/storage/app/public storage/app/public +``` + +⚠️ Mit dem Symlink landen **Uploads aus der Vorschau in den Live-Dateien**. Zum Anschauen in +Ordnung, zum Testen von Uploads nicht. Im Zweifel kopieren. + +--- + +## 4. `.env` der Vorschau anpassen + +Der wichtigste Schritt. Der `ThemeServiceProvider` sucht den aufgerufenen Host in +`config/domains.php` und setzt `URL::forceRootUrl()` samt Asset-Origin auf die dort hinterlegte +URL. Fehlt `DOMAIN_B2IN_URL`, greift der Vorgabewert **`https://b2in.test`** – eine Domain, die es +im Internet nicht gibt. Dann laden Bilder, Favicon und `livewire.min.js` nicht mehr. + +```bash +cd /var/www/b2in-preview +nano .env +``` + +Diese Werte ändern: + +```dotenv +APP_ENV=preview +APP_DEBUG=false +APP_URL=https://preview.b2in.eu +APP_DOMAIN_NAME=preview.b2in.eu + +# Vorschau-Domain, damit Routing, Links und Assets stimmen +DOMAIN_B2IN=preview.b2in.eu +DOMAIN_B2IN_URL=https://preview.b2in.eu + +# Assets ebenfalls auf die Vorschau-Domain, sonst laedt der Browser sie von woanders +ASSET_URL=https://preview.b2in.eu + +# Eigene Datenbank +DB_DATABASE=b2in_preview +DB_USERNAME=… +DB_PASSWORD=… + +# Eigener Cache-/Session-Namensraum, damit sich Live und Vorschau nicht ins Gehege kommen +CACHE_PREFIX=b2in_preview +SESSION_COOKIE=b2in_preview_session + +# Keine Mails aus der Vorschau verschicken +MAIL_MAILER=log +``` + +`APP_ENV=preview` sorgt automatisch für `noindex, nofollow` in jeder Seite +(`config/app.php` → `app.indexable`). Wer ganz sichergehen will, setzt zusätzlich +`SITE_INDEXABLE=false`. + +**Nicht ändern:** `APP_KEY` – sonst sind verschlüsselte Werte in der kopierten Datenbank +nicht mehr lesbar. + +--- + +## 5. Datenbank der Vorschau befüllen + +```bash +mysql -u USER -p -e "CREATE DATABASE b2in_preview CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +mysql -u USER -p b2in_preview < ~/b2in_live_JJJJ-MM-TT_HHMM.sql +``` + +Gegenprobe, dass die Vorschau wirklich auf ihrer eigenen DB steht: + +```bash +cd /var/www/b2in-preview +php artisan tinker --execute="echo config('database.connections.mysql.database');" +# muss b2in_preview ausgeben – wenn hier b2in_live steht: STOPP +``` + +--- + +## 6. Update einspielen + +Das Archiv `dev/2026-08-28/deploy/b2in-interior-update.tar.gz` enthält genau die geänderten und +neuen Dateien plus die gebauten Assets (`public/build/b2in`, `public/build/portal`). **Node wird +auf dem Server nicht gebraucht.** Die enthaltene Dateiliste steht in `deploy/dateiliste.txt`. + +```bash +# Archiv hochladen, dann: +cd /var/www/b2in-preview +tar xzf ~/b2in-interior-update.tar.gz +``` + +Das Archiv enthält bewusst **keine** `.env`, kein `vendor/` und nichts aus `storage/`. + +--- + +## 7. Artisan-Schritte + +```bash +cd /var/www/b2in-preview + +php artisan config:clear +php artisan storage:link + +php artisan migrate --force # legt cms_brands an, ergänzt summary/highlights + +# Kernmarken anlegen – überschreibt nichts, was schon gepflegt ist +php artisan db:seed --class=CmsBrandSeeder --force + +# Die neuen Texte ins CMS: genau die 23 umgetexteten Sektionen +php artisan db:seed --class=InteriorRelaunchCmsSeeder --force + +# Magazin: die zwei neuen Beiträge plus Reihenfolge +php artisan db:seed --class=CmsArticleSeeder --force + +# Markenseiten CABINET, Rotpunkt und Febrü – nur Textfelder, Bilder und Galerie bleiben +php artisan db:seed --class=BrandPageTextSeeder --force + +# Entfernt den Vermerk „BEISPIEL – “ aus vorhandenen Bildunterschriften (Nachtrag 5). +# Nur nötig, wenn die Demogalerien schon eingespielt sind. Mehrfach ausführbar. +php artisan db:seed --class=BrandGalleryCaptionNormalizerSeeder --force + +# Markenlogos nachtragen. Setzt `logo` nur, wenn das Feld leer ist. +php artisan db:seed --class=BrandLogoSeeder --force + +# Display-Branding auf das Interior-Logo +php artisan db:seed --class=DisplayInteriorBrandingSeeder --force + +# Medien-URLs host-unabhängig (sonst zeigen vier Playlists auf b2in.test) +php artisan db:seed --class=DisplayMediaUrlNormalizerSeeder --force + +php artisan config:cache +php artisan route:cache +php artisan view:clear +``` + +> **Assets:** Das Archiv bringt `public/build/b2in` fertig mit. Wird auf dem Server selbst +> gebaut, gilt: `npm run build:web` baut **nur** die vier Frontend-Themes, nicht das Portal. +> Ohne `npm run build:portal` liefert das Admin-Portal danach 500. + +**Optional, nur für die Abstimmung:** Beispielbilder in die Markengalerien, damit Galerie, +Referenzen-Seite und Lightbox nicht leer sind. + +> ⚠️ Es sind Stockmotive und KI-Renderings, kein einziges echtes Projekt. Der frühere Vermerk +> „BEISPIEL" in der Bildunterschrift ist auf Kundenwunsch entfallen – die Bilder der Rubrik +> „Realisiertes Projekt" geben sich damit als echte Referenzen aus. Für die Abstimmung in +> Ordnung, **vor dem Livegang zwingend ersetzen**. + +```bash +php artisan db:seed --class=DemoBrandGallerySeeder --force +``` + +Wieder entfernen: + +```bash +php artisan tinker --execute="App\Models\CmsBrand::query()->update(['gallery' => null]);" +``` + +--- + +## 8. Vorschau abschirmen + +**Suchmaschinen** – `noindex` kommt schon aus `APP_ENV=preview`, die `robots.txt` gehört +zusätzlich umgestellt: + +```bash +printf 'User-agent: *\nDisallow: /\n' > /var/www/b2in-preview/public/robots.txt +``` + +**Passwortschutz** (empfohlen, damit die Vorschau nicht zufällig gefunden wird): + +```bash +htpasswd -c /etc/apache2/.htpasswd-preview vorschau +``` + +Apache: + +```apache + + AuthType Basic + AuthName "B2in Vorschau" + AuthUserFile /etc/apache2/.htpasswd-preview + Require valid-user + +``` + +Nginx: + +```nginx +auth_basic "B2in Vorschau"; +auth_basic_user_file /etc/nginx/.htpasswd-preview; +``` + +--- + +## 9. Prüfen + +| Seite | Erwartung | +|---|---| +| `/` | Hero → Marken-Teaser → Vision → Sponsoring → Showroom-Display → Immobilien → CTA | +| `/marken` | Grauer Seitenkopf mit Bild, drei gerahmte Markenblöcke | +| `/marken/cabinet` | Detailseite, Galerie mit den zwei Rubriken (falls Demo-Seeder lief) | +| `/marken/rotpunkt` | Neun Abschnitte, vier davon mit aufklappbarer Vertiefung; Button unten rechts heißt „Persönliche Küchenberatung vereinbaren" | +| `/marken/februe` | Elf Abschnitte, Zitat von Marcel; Button heißt „Persönliche Arbeitsplatzberatung vereinbaren" | +| `/referenzen` | Realisierte Projekte, darunter das Showroom-Display im Hochformat | +| `/magazin` | Die zwei Interior-Beiträge stehen oben | +| `/faq` | Rubriken „Für Ihr Zuhause" und „Für Projekte und Objekte" | +| `/about` | Werte: Beratung statt Verkauf, Maßarbeit, … | +| `/contact` | Betreffliste beginnt mit Beratungstermin | +| `/immobilien` | unverändert | + +Und diese vier Punkte: + +- [ ] Terminanfrage-Widget unten rechts öffnet sich, Absenden zeigt die Bestätigung +- [ ] Alle Links bleiben auf `preview.b2in.eu` – **springt einer auf `b2in.eu`, stimmt Schritt 4 nicht** +- [ ] Seitenquelltext enthält `` +- [ ] Die Live-Seite `https://b2in.eu` sieht unverändert aus + +--- + +## 10. Aufräumen + +Vorschau wieder abbauen: + +```bash +rm -rf /var/www/b2in-preview +mysql -u USER -p -e "DROP DATABASE b2in_preview;" +# vhost und DNS-Eintrag entfernen +``` + +Beim späteren echten Livegang gilt **Abschnitt 8 des Entwicklungsplans**, nicht dieses Dokument. +Vorher offen: Rechtstexte, OG-Bild, Seeder-Passwort rotieren. + +--- + +## Wenn etwas nicht stimmt + +| Symptom | Ursache | +|---|---| +| **„Kein Server mit dem angegebenen Hostnamen gefunden" für Bilder, Favicon, `livewire.min.js`** | `DOMAIN_B2IN_URL` fehlt oder ist falsch geschrieben → Vorgabewert `https://b2in.test`. Siehe Diagnose unten. | +| Links springen auf `b2in.eu` | `DOMAIN_B2IN` / `DOMAIN_B2IN_URL` in Schritt 4 nicht gesetzt, oder `config:cache` fehlt | +| Seite ohne Styles | `public/build/b2in` fehlt – Archiv nicht vollständig entpackt | +| „Vite manifest not found" | dasselbe, oder `build/portal` fehlt für das Admin-Panel | +| Bilder fehlen | `php artisan storage:link` vergessen, oder `storage/app/public` nicht mitkopiert | +| Display-Kachel bleibt leer | Kein aktives Display mit veröffentlichter Playlist – dann wird die Sektion bewusst gar nicht angezeigt | +| Änderungen wirken nicht | `php artisan config:clear && php artisan config:cache` | + +### Diagnose: Assets zeigen auf den falschen Host + +```bash +cd /var/www/b2in-preview + +# 1. Was steht in der .env? +grep -E '^(APP_ENV|APP_URL|APP_DOMAIN_NAME|ASSET_URL|DOMAIN_B2IN|DOMAIN_B2IN_URL)=' .env + +# 2. Was macht die Anwendung daraus? (zeigt den zwischengespeicherten Stand) +php artisan tinker --execute="echo config('domains.domains.b2in.domain_name').' -> '.config('domains.domains.b2in.url').PHP_EOL;" + +# 3. Liegt ein alter Config-Cache herum? +ls -l bootstrap/cache/config.php 2>/dev/null +``` + +Steht in Zeile 2 etwas mit `.test`, fehlen die beiden `DOMAIN_B2IN*`-Zeilen. Beheben: + +```bash +cd /var/www/b2in-preview +php artisan config:clear # zuerst den Cache weg, sonst wirkt die .env nicht +nano .env # DOMAIN_B2IN, DOMAIN_B2IN_URL, ASSET_URL, APP_URL setzen +php artisan config:cache +php artisan view:clear +``` + +**Sicherheitsnetz seit dieser Runde:** Findet der `ThemeServiceProvider` den aufgerufenen Host +nicht in der Konfiguration, verwendet er außerhalb von `APP_ENV=production` den tatsächlich +aufgerufenen Origin – die Vorschau verlinkt sich dann auf sich selbst statt auf eine fremde +Domain. In `production` bleibt es bei der konfigurierten URL, damit ein gefälschter Host-Header +keine Links auf fremde Domains erzeugen kann. Die `.env` sollte trotzdem stimmen. diff --git a/dev/2026-08-28/sponsoren-logos/Arminia_Club_1905.png b/dev/2026-08-28/sponsoren-logos/Arminia_Club_1905.png new file mode 100644 index 00000000..5af48d6d Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/Arminia_Club_1905.png differ diff --git a/dev/2026-08-28/sponsoren-logos/TCBWH_Logo.jpg b/dev/2026-08-28/sponsoren-logos/TCBWH_Logo.jpg new file mode 100644 index 00000000..5516b8e2 Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCBWH_Logo.jpg differ diff --git a/dev/2026-08-28/sponsoren-logos/TCH.jpg b/dev/2026-08-28/sponsoren-logos/TCH.jpg new file mode 100644 index 00000000..935d1568 Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCH.jpg differ diff --git a/dev/2026-08-28/sponsoren-logos/TCH.png b/dev/2026-08-28/sponsoren-logos/TCH.png new file mode 100644 index 00000000..8cc1a9d0 Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCH.png differ diff --git a/dev/2026-08-28/sponsoren-logos/TCH_Logo.jpg b/dev/2026-08-28/sponsoren-logos/TCH_Logo.jpg new file mode 100644 index 00000000..2e8d4d6d Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCH_Logo.jpg differ diff --git a/dev/2026-08-28/sponsoren-logos/TCH_Logo.png b/dev/2026-08-28/sponsoren-logos/TCH_Logo.png new file mode 100644 index 00000000..2deb9b11 Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCH_Logo.png differ diff --git a/dev/2026-08-28/sponsoren-logos/TCH_Logo_Year.jpg b/dev/2026-08-28/sponsoren-logos/TCH_Logo_Year.jpg new file mode 100644 index 00000000..363238fc Binary files /dev/null and b/dev/2026-08-28/sponsoren-logos/TCH_Logo_Year.jpg differ diff --git a/dev/2026-08-28/umsetzung-03-09-2026.md b/dev/2026-08-28/umsetzung-03-09-2026.md new file mode 100644 index 00000000..1c8838ab --- /dev/null +++ b/dev/2026-08-28/umsetzung-03-09-2026.md @@ -0,0 +1,16 @@ +Folgende Inhalte müssen noch umgesetzt werden: + + + +# 1. Bei den Marken soll die Detailseite umfangreicher werden. Hier gibt es bereits zwei Texte, die final abgestimmt sind. +Neue Rotpunkt-Texte einpflegen +dev/2026-08-28/Rotpunkt_Webseitentext_FINAL.md + +Neue Febrü-Texte einpflegen +dev/2026-08-28/Februe_Webseitentext_FINAL.md +CABINETT +Im gleichen Aufbau, wie die zwei Markenseiten für CABINETT bauen und mit eigenen Texten bestücken, erstelle die Texte entsprechend den anderen Vorlagen ab, passend zu CABINETT + +# 2. „BEISPIEL"-Kennzeichnung bei Visualisierungen entfernen +# 3. Hinweis zu Visualisierungen eleganter formulieren +# 4. Abschluss-CTA hochwertiger durch neue Texte bereits erledigt („Ihre Küche. Persönlich geplant."), aber auch optisch verbessern \ No newline at end of file diff --git a/dev/2026-08-28/umsetzung-inhalte.md b/dev/2026-08-28/umsetzung-inhalte.md new file mode 100644 index 00000000..47523d7d --- /dev/null +++ b/dev/2026-08-28/umsetzung-inhalte.md @@ -0,0 +1,338 @@ +# B2in Interior – Umsetzungsdokument Inhalte + +**Arbeitsdokument für die Umsetzung.** Für jede Sektion: aktueller Stand → neuer Inhalt. +Schreibweisen verbindlich: **Interior** (nicht Interieur), **Febrü**, **Rotpunkt Küchen**, **CABINET** (Versalien). + +Platzhalter sind mit `[PH]` markiert – dort fehlt Zulieferung von Marcel. + +--- + +## 0. Globale Änderungen + +### Meta / SEO + +| | Alt | Neu | +|---|---|---| +| **Title** | B2IN - Connecting Design and Property | B2in Interior – Premium-Einrichtung & Maßanfertigung \| CABINET, Rotpunkt, Febrü | +| **Description** | B2in verbindet exklusive Immobilien in Dubai mit europäischem Einrichtungsdesign. Ihr Partner für Investment & Interior. | B2in Interior verbindet starke Einrichtungsmarken mit persönlicher Beratung – von der Maßanfertigung bis zur vollständigen Ausstattung für Zuhause und Projekte. | +| **OG-Title** | wie Title | wie neuer Title | +| **OG-Description** | wie Description | wie neue Description | +| **OG-Image** | b2in-og-default.jpg (Investment-Optik) | `[PH]` Interior-Motiv – vorerst bestehendes Hero-Bild verwenden | + +### Logo + +- Wortmarke **B2in** bleibt, Zusatz **Interior** in leichterer Schriftstärke darunter oder rechts daneben. +- Betrifft: `b2in-logo-positive.svg` (Header) und `b2in-logo-negative.svg` (Footer). +- `[PH]` Falls Marcel keine Datei liefert: Zusatz typografisch mit vorhandener Schrift ergänzen, sauber ausgerichtet. + +### Navigation + +| Alt | Neu | +|---|---| +| Home · Immobilien · Netzwerk · Magazin · FAQ · Über B2in | Home · **Marken** · **Referenzen** · Magazin · Immobilien · Über B2in | + +- **Marken** = neue Kernmarken-Seite (Anker auf Startseite oder eigene Seite) +- **Referenzen** = die selbst pflegbare Rubrik (Web-Display) +- **Netzwerk** entfällt aus der Hauptnavigation (Inhalt geht in Marken/Über B2in auf), Seite bleibt erreichbar +- **FAQ** in den Footer verschieben oder unter „Über B2in" einhängen +- **Immobilien** rutscht nach hinten, bleibt aber in der Navigation + +--- + +## 1. Ankündigungsleiste + +**Alt:** +> NEW LAUNCH Azizi Creek Views 4 – Exklusives Off-Market-Projekt in Dubai ab 1.125.000 AED (ca. 283.000 EUR / 306.000 USD) → Exposé ansehen + +**Neu (Startbelegung):** +> **NEU** Rotpunkt Küchen jetzt bei B2in Interior – jetzt Beratungstermin sichern → Termin anfragen + +**Alternative, falls Rotpunkt-Studio noch nicht startklar:** +> **NEU** CABINET Einbauschränke nach Maß – Showroom Bielefeld → Termin anfragen + +*Hinweis: Leiste bleibt technisch wie sie ist, nur Inhalt und Ziel-Link ändern sich. Dubai-Launches können dort später wieder erscheinen, aber nicht als Dauerzustand.* + +--- + +## 2. Hero + +**Alt:** +> # B2in – Wo exklusive Immobilien-Investments auf smartes Interior treffen. +> Ihr Partner für renditestarke internationale Immobilien und globales Supply-Chain-Management im Interior-Bereich. +> [Zu den Immobilien-Projekten] [Unser Netzwerk] +> Internationale Immobilien · Exklusive Einrichtung · Persönliche Beratung + +**Neu:** + +> # B2in Interior – Premium-Einrichtung von starken Marken. Für Ihr Zuhause. Für Ihr Projekt. +> +> Von der einzelnen Maßanfertigung bis zur vollständigen Ausstattung – mit CABINET, Rotpunkt Küchen, Febrü und einem Netzwerk regionaler Fachexperten. + +**Buttons:** +- Primär (dunkel): **Unsere Marken entdecken** → `#marken` +- Sekundär (hell): **Termin anfragen** → öffnet Anfrage-Widget + +**Checkmarks:** +- Premium-Marken +- Maßgefertigte Lösungen +- Persönliche Beratung + +**Hero-Bild:** `[PH]` Interior-Motiv (Einbaumöbel oder Küche, warmes Licht). Bis zur Zulieferung bestehendes Bild belassen. + +**Bild-Badge (unten rechts über dem Bild):** + +| Alt | Neu | +|---|---| +| B2in / Connecting Design and Property | B2in Interior / Einrichtung, die bleibt. | + +--- + +## 3. Marcel-Badge (kleiner Einschub unter dem Hero) + +**Alt:** +> B2in by Marcel Scheibe – Ihr persönlicher Partner für internationale Immobilien und exklusive Einrichtungskonzepte. +> Marcel Scheibe · Gründer & CEO, B2in + +**Neu:** +> B2in by Marcel Scheibe – Ihr persönlicher Partner für hochwertige Einrichtung und starke Marken. +> Marcel Scheibe · Gründer & CEO, B2in + +--- + +## 4. Weiche Privat / Geschäft + +**Ersetzt vollständig die Sektion „Zwei Welten. Ein Netzwerk."** + +**Alt:** +> ## Zwei Welten. Ein Netzwerk. +> Wir verbinden den Immobilienkauf mit der perfekten Einrichtung. **Immobilien-Investoren** profitieren von unserem exklusiven Möbel-Netzwerk – **Projektentwickler** von unserer deutschen Vertragssicherheit im Supply-Chain-Management. + +**Neu:** + +> ## Für Ihr Zuhause. Für Ihr Projekt. +> +> Ob einzelnes Möbelstück oder komplette Ausstattung – wir begleiten Sie mit denselben Marken, demselben Anspruch und einem persönlichen Ansprechpartner. + +**Karte 1** + +> ### Einrichtung für Ihr Zuhause +> +> Einzelne Möbelstücke, maßgefertigte Einbaulösungen oder die komplette Einrichtung: Unsere Kernmarken und unser Fachhandels-Netzwerk begleiten Sie von der ersten Idee bis zur Montage. Besuchen Sie unsere Showrooms oder vereinbaren Sie eine persönliche Beratung. +> +> **Button:** Beratung anfragen + +**Karte 2** + +> ### Ausstattung für Ihr Projekt +> +> Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk. +> +> **Button:** Projekt besprechen + +*Bild „Das B2in-Ökosystem" aus der alten Sektion entfällt hier – kann bei „Über B2in" weiterverwendet werden.* + +--- + +## 5. Kernmarken (neues CMS-Modul) — `#marken` + +> ## Unsere Kernmarken +> +> Wir arbeiten nicht mit beliebigen Katalogen, sondern mit ausgewählten Herstellern, hinter denen wir stehen – jede Marke ein Spezialist auf ihrem Gebiet. + +### Kachel 1 – CABINET + +> ### CABINET +> **Einbauschränke nach Maß.** +> +> Maßgefertigte Schranklösungen, geplant bis auf den Millimeter – gefertigt für Räume, in denen Standard nicht genügt. Erleben Sie die Möglichkeiten in unserem Showroom in Bielefeld. +> +> *Mehr zu CABINET →* + +### Kachel 2 – Rotpunkt Küchen + +> ### Rotpunkt Küchen +> **Küchen, die Haltung zeigen.** +> +> Premium-Küchen aus deutscher Fertigung – individuell geplant, nachhaltig gedacht und gebaut für den Mittelpunkt Ihres Zuhauses. Beratung und Planung in unserem Küchenstudio. +> +> *Mehr zu Rotpunkt →* + +### Kachel 3 – Febrü + +> ### Febrü +> **Arbeiten, wo man gerne ist.** +> +> Möbel für konzentriertes Arbeiten – vom durchdachten Home-Office bis zur kompletten Büroeinrichtung. Ergonomie und Design für Zuhause und fürs Unternehmen. +> +> *Mehr zu Febrü →* + +### Detailseiten (Vorlage-Struktur je Marke) + +Jede Marke bekommt eine Detailseite mit: +1. Header (Logo + Claim + Stimmungsbild) +2. Einleitungstext (3–5 Sätze) +3. Leistungen / Besonderheiten (3–4 Punkte) +4. Bildergalerie +5. CTA-Block: „Termin anfragen" + „Per WhatsApp schreiben" + +`[PH]` Logos und Basis-Infos zu Rotpunkt und Febrü fehlen – Detailseiten vorerst mit obigen Kacheltexten plus Platzhalter-Galerie anlegen, damit die Struktur steht. + +--- + +## 6. Über Marcel / Vertrauen + +**Alt:** +> ## Gebaut auf Expertise und Vertrauen +> **B2in (Bridges2international)** verbindet zwei Welten: internationale Immobilien und exklusive Einrichtungskonzepte. Als Ihr persönlicher Partner navigiere ich Sie durch beide Bereiche – mit Expertise, Netzwerk und dem Anspruch, dass jede Entscheidung auf Vertrauen basiert. +> Ob ein **Investment in Dubai**, eine Villa in Lissabon oder die **maßgeschneiderte Einrichtung** Ihres neuen Zuhauses durch lokale Fachexperten – bei B2in laufen alle Fäden zusammen. +> **Regional verwurzelt, international vernetzt** – das ist B2in. + +**Neu:** + +> ## Gebaut auf Expertise und Vertrauen +> +> **B2in** stand ursprünglich für *Bridges to International*. Heute steht es ebenso für *Bridges to Interior*: hochwertige Einrichtung mit persönlicher Handschrift. Als Ihr Ansprechpartner verbinde ich ausgewählte Hersteller, regionale Fachexperten und ein verlässliches Netzwerk – damit aus Räumen Orte werden, die bleiben. +> +> Ob **maßgefertigter Einbauschrank**, neue **Küche** oder die **vollständige Ausstattung** eines Objekts: Bei B2in laufen alle Fäden zusammen. Und wer über die Einrichtung hinausdenkt, findet bei uns auch den Zugang zu internationalen Immobilien. +> +> **Regional verwurzelt, international vernetzt** – das ist B2in. + +*Hier ist die Markenstory verankert, die Marcel selbst vorgeschlagen hat. Immobilien bewusst als vorletzter Satz.* + +--- + +## 7. Engagement / Sponsoring + +**Neue Sektion, direkt nach „Über Marcel" – schlanke Logo-Leiste.** + +> ## Regional verbunden +> +> Wir unterstützen Vereine und Organisationen aus der Region – weil Verwurzelung mehr ist als eine Adresse. + +**Logos:** 3 gelieferte Sponsoring-Logos in einer ruhigen, gleich großen Reihe (Graustufen oder gedeckt, damit sie den Markenauftritt nicht überstrahlen). + +`[PH]` Weitere Logos ergänzen, sobald geliefert. + +--- + +## 8. Aktuelles / Referenzen (Web-Display) + +**Ersetzt die Sektion „Ein Ökosystem, drei Säulen" komplett.** + +**Alt (entfällt):** +> ## Ein Ökosystem, drei Säulen +> Immobilien & Investments / Local-for-Local Marktplatz / Supply-Chain-Management + +*Begründung: Zwei von drei Säulen sind interne Konzeptsprache, die Besucher nicht verstehen, und die dahinterliegenden Systeme sind pausiert.* + +**Neu – Startseiten-Teaser:** + +> ## Aktuelles aus unseren Showrooms +> +> Projekte, Neuheiten und Einblicke – laufend aktualisiert. +> +> **Button:** Alle Referenzen ansehen → + +**Neue Unterseite `/referenzen`:** + +> # Referenzen +> +> Realisierte Projekte aus unseren Showrooms und von unseren Kunden. Jedes Projekt maßgefertigt, jedes anders. + +Inhalte kommen aus dem Web-Display im Display-CMS (Bild, Text, optional YouTube-Video), von Marcel selbst pflegbar. + +`[PH]` Startbelegung mit CABINET-Projekten durch Marcel. Bis dahin 2–3 Platzhalter-Einträge anlegen, damit der Bereich nicht leer wirkt. + +**Wichtig:** Unter „Referenzen" ausschließlich realisierte Projekte. Visualisierungen und Entwürfe gehören in die Kernmarken-Detailseiten, nicht hierher. + +--- + +## 9. Immobilien-Teaser + +**Neu, klein und dezent, kurz vor dem Abschluss-CTA:** + +> ## Außerdem: Internationale Immobilien +> +> Neben der Einrichtung begleiten wir ausgewählte internationale Immobilienprojekte – persönlich kuratiert und mit klarem Blick auf Substanz. +> +> **Link (dezent):** Zu den Immobilien → + +*Keine Preise, keine Renditeversprechen, kein Dubai im Teasertext. Wer gezielt sucht, findet den Weg.* + +--- + +## 10. Abschluss-CTA + +**Alt:** +> ## Ihr nächster Schritt +> Ob Immobilien-Investment, Supply-Chain-Partnerschaft oder Einrichtungs-Netzwerk – sprechen Sie direkt mit uns. +> [Kontakt aufnehmen] + +**Neu:** + +> ## Ihr nächster Schritt +> +> Ob einzelnes Möbelstück, komplette Einrichtung oder Projektausstattung – sprechen Sie mit uns. Persönlich, unkompliziert, verbindlich. +> +> **Button 1:** Termin anfragen (öffnet Widget) +> **Button 2:** Per WhatsApp schreiben (`wa.me`-Link) + +--- + +## 11. Footer + +| | Alt | Neu | +|---|---|---| +| **Claim unter Logo** | Connecting Design and Property | Premium-Einrichtung. Persönlich gemacht. | +| **Zeile darunter** | Marcel Scheibe – Gründer & CEO | bleibt | + +*Falls Marcel „Bridges to Interior" als Claim bevorzugt, wird stattdessen das eingesetzt.* + +--- + +## 12. Terminanfrage-Widget + +Schwebendes Element unten rechts, auf allen Seiten. + +**Öffnet mit:** +> **Termin anfragen** +> Kurz Ihre Daten, wir melden uns persönlich zurück. + +**Felder:** +- Name *(Pflicht)* +- E-Mail *(Pflicht)* +- Telefon *(optional)* +- Anliegen / Marke *(Auswahl: CABINET · Rotpunkt Küchen · Febrü · Projektausstattung · Sonstiges)* +- Wunschtermin *(optional)* +- Nachricht *(optional)* + +**Button:** Anfrage senden +**Bestätigung:** Danke für Ihre Anfrage – wir melden uns zeitnah bei Ihnen. + +**Zweiter Button daneben:** WhatsApp → öffnet Chat direkt. + +--- + +## Arbeitsreihenfolge (Vorschlag) + +1. **Globales zuerst:** Logo-Zusatz, Meta/SEO, Navigation, Footer-Claim +2. **Startseite von oben nach unten:** Ankündigungsleiste → Hero → Weiche → Kernmarken → Über Marcel → Sponsoring → Aktuelles → Immobilien-Teaser → Abschluss +3. **Kernmarken-Modul im CMS** anlegen und mit den drei Marken befüllen +4. **Referenzen-Rubrik** (Web-Display) anlegen, 2–3 Platzhalter einstellen +5. **Terminanfrage-Widget + WhatsApp-Button** +6. **Durchgang auf Alt-Begriffe:** „Supply-Chain", „Local-for-Local", „Ökosystem", „Investment" auf Unterseiten prüfen und bereinigen (auch /netzwerk, /about, /faq) + +--- + +## Offene Zulieferungen von Marcel + +| Was | Status | +|---|---| +| Sponsoring-Logos | ✅ 3 Stück erhalten | +| Logo mit Zusatz „Interior" | ❌ offen – ggf. selbst ergänzen | +| Rotpunkt: Logo + Basis-Infos | ❌ offen – Platzhalter | +| Febrü: Logo + Basis-Infos | ❌ offen – Platzhalter | +| Hero-Bildmotiv (Interior) | ❌ offen – Bestandsbild belassen | +| Bilder Kernmarken-Kacheln | ❌ offen – Platzhalter | +| CABINET-Referenzprojekte | ❌ offen – Platzhalter-Einträge | +| WhatsApp-Nummer für Button | ❌ klären: Mobil +49 151 51002992? | \ No newline at end of file diff --git a/dev/2026-08-28/umsetzungen-14-09-2026.md b/dev/2026-08-28/umsetzungen-14-09-2026.md new file mode 100644 index 00000000..0908977c --- /dev/null +++ b/dev/2026-08-28/umsetzungen-14-09-2026.md @@ -0,0 +1,186 @@ +# B2in Interior – Arbeitsplan + +**Freigegebener Umfang, Stand September 2026** + +--- + +## Freigabestatus im Überblick + +| Phase | Status | Aufwand | +|---|---|---:| +| **1 – Website: Layout, Marken, Herkunft** | ✅ freigegeben | 39–65 h | +| **3 – QR-Empfehlungssystem** | ✅ freigegeben | 25–39 h | + +--- + +# Phase 1 – Website + +## 1.1 Farbwelt und Logo + +| Aufgabe | +| Interior-Farbwelt im Themensystem umstellen: +Tanne `#202c2b` Hauptton dunkel, Wortmarke, +Creme `#f7f4ef` Hintergrund, +Sand `#d9cbbf` Flächen, Footer, +Ocker `#d39e46` Akzent, Logo-Bogen +Clay `#a38f82` Sekundär, Produkte +Anthrazit `#2d2926` Text +| Immobilien-Welt als eigene Farbwelt abgrenzen (bestehendes Blau) | +| Logo-Varianten mit farbigem Bogen je Welt, positiv und negativ - normal Immobilien und Interior mit Zusatz +hier liegen die Logos dev/2026-08-28/logos +| Welten-Wechsler in Topleiste und Footer, Immobilien und Interior | + Bei der Umsetzung auf die neue Farbwelt bitte auch diese Shop Screenshots einmal begutachten. In diese Richtung soll es gehen optisch _b2in_interior_shop/screenshots/web + +## 1.2 Tischlerei als vierte Marke + +| Aufgabe | +| Markenseite anlegen, CMS-Eintrag, Einbindung in `/marken` und Navigation | +Als Inhalt erst mal Platzhalter. + +**Inhaltliche Ausrichtung:** Aufbau, Sonderlösungen, Veredelung, Maßarbeit. Ergänzt CABINET und Rotpunkt, steht nicht daneben. + +**Offen:** Name der Marke, Logo, Basis-Infos, Bildmaterial. Zur Namensfrage: Tritt sie unter eigenem Namen auf oder als B2in-Leistung? Der Name des Partnerbetriebs gehört auf die Detailseite, nicht in die Marke. + +## 1.3 Herkunfts-Sektion + +| Aufgabe | +| Eigener Bereich „Aus der Region gefertigt" mit den vier Standorten, visuell aufbereitet | + +**Die Standorte:** Rotpunkt in Bünde, Febrü in Herford, Tischlerei in Rheda-Wiedenbrück, CABINET in Köln. Vier deutsche Manufakturen, drei davon in Ostwestfalen. + +**Zu klären:** Ist CABINET Hersteller oder Vertrieb? Die Formulierung muss sauber trennen zwischen gefertigt und vertrieben. + +## 1.4 Markenseiten und Texte + +| Aufgabe | +| Aufklappbare Detailbereiche auf den Markenseiten | +| Kontextbezogene Beschriftung des Terminanfrage-Widgets je Marke | + +**Texte:** Die finalen Fassungen für Rotpunkt und Febrü Sind bereits eingearbeitet. + +## 1.5 Abschluss + +| Aufgabe | Std. | +|---|---:| +| Tests, Prüfung bestehender Bereiche, Produktivsetzung | + +**Summe Phase 1: 39–65 h** · rund 3.510 – 5.850 € netto + +--- + +# Phase 3 – QR-Empfehlungssystem + +## Wie es funktioniert Linus + +1. Partner im System anlegen (Makler, Architekt, Handwerker, Praxis) +2. System erzeugt eindeutigen Code und QR als Bilddatei +3. Marcel druckt den Aufkleber selbst und bringt ihn auf dem Flyer an +4. Interessent scannt, landet auf der Website, Zugriff wird erfasst +5. Startet er eine Anfrage, ist sie dem Partner zugeordnet +6. Übersicht im Admin: Partner, Scans, Anfragen + +## Die Bausteine + +| Aufgabe | Std. | +|---|---:| +| Datenmodell: Partner, Code, Zuordnung, Status | 3–5 | +| Partnerverwaltung im Admin, Code erzeugen, QR als Bilddatei herunterladen | 7–10 | +| Weiterleitung und Erfassung des Scans | 5–8 | +| Zuordnung der Anfrage zum Partner, angebunden an die bestehende Anfragestrecke | 4–6 | +| Einfache Übersicht im Admin: Partner, Scans, Anfragen | 3–5 | +| Tests | 3–5 | + +**Summe Phase 3: 25–39 h** · rund 2.250 – 3.510 € netto + +## Nicht enthalten + +Provisionsabrechnung und Auszahlung, Partner-Login (Phase 4), Aufkleber-Vorlagen als fertiges PDF, ausgebaute Auswertung mit Diagrammen oder Filtern. + +## Technische Vorgaben für den Flyer + +| | | +|---|---| +| **Aufkleber** | 60 × 80 mm hochkant. QR bei 30 × 30 mm, darunter Partnername, darunter Platz für eine Zeile | +| **Freie Fläche auf dem Flyer** | 70 × 90 mm | +| **Untergrund** | hell, einfarbig, kein Bild, kein Verlauf | +| **Abstand** | mindestens 10 mm zu Rand und Falz | +| **Platzierung** | Rückseite, in der Nähe der Kontaktangaben | + +Die Zielgruppen-Ansprache steckt im Aufkleber, nicht im Flyer. Damit bleibt der Flyer eine Druckfassung. + +--- + +# Phase 2 – Flyer: was enthalten ist + +**2 Stunden.** Eine strukturierte schriftliche Rückmeldung auf einen fertigen Entwurf: Aufbau, Reihenfolge der Inhalte, Wirkung, Markenkonformität, Textschärfe. Einmal, als zusammenhängende Liste. + +**Nicht enthalten:** mehrfache Durchgänge über Zwischenstände, Layoutkorrekturen, Bildauswahl, Umschreiben von Texten, Druckdatenaufbereitung. + +--- + +# Reihenfolge der Umsetzung + +| Schritt | Was | Warum zuerst | +|---|---|---| +| **1** | Farbwelt und Logo | Fundament – alles Weitere baut darauf auf | +| **2** | Welten-Wechsler | gehört technisch zur Farbwelt-Umstellung | +| **3** | Tischlerei-Markenseite | braucht die fertige Farbwelt | +| **4** | Herkunfts-Sektion | braucht die Tischlerei als vierten Standort | +| **5** | Akkordeon und Widget-Beschriftung | Feinschliff auf den fertigen Seiten | +| **6** | Tests und Livegang Phase 1 | | +| **7** | Phase 3 komplett | erst sinnvoll, wenn die Website steht | + +Phase 3 erst nach dem Livegang von Phase 1: Ein QR-Code, der auf eine halbfertige Seite führt, verbrennt den ersten Eindruck beim Partner. + +--- + +# Was ich von Marcel brauche + +## Blockierend für Phase 1 + +| Was | Für | +|---|---| +| Freigabe Farbwerte (Ocker, Tanne) | 1.1 – Start | +| Name der Tischlerei-Marke | 1.2 | +| Logo und Basis-Infos Tischlerei | 1.2 | +| Bildmaterial Tischlerei | 1.2 | +| Klärung: CABINET Hersteller oder Vertrieb | 1.3 | + +## Nicht blockierend, aber offen + +| Was | Anmerkung | +|---|---| +| Logos Rotpunkt und Febrü | für die Markenseiten | +| Bildmaterial Hero und Kernmarken | Platzhalter möglich | +| Neuer Footer-Claim | „Connecting design and property" ist der alte Immobilien-Claim | +| Showroom-Adressen je Markenseite | nennen beide Seiten den richtigen Standort? | +| Höhe der Partnervergütung | vor der Partner-Ansprache, nicht vor dem Bau | + +--- + +# Empfehlungen fürs Protokoll + +**Erste zehn Partner:** Makler, Architekten, Handwerker. Praxen danach. Keine Wertung, sondern Priorisierung auf den Kanal mit der höheren Wirkung. + +**Keine Provisionen an Arztpraxen** im ersten Schritt. Flyer dort über die persönliche Verbindung auslegen, mit eigenem QR-Code. Nach drei Monaten zeigen die Zahlen, ob der Kanal trägt. + +**QR-Feld neutral halten.** Sachliche Zeile wie „Beratungstermin vereinbaren". Keine emotionalen Appelle. + +--- + +# Zurückgestellt + +| Thema | Status | +|---|---| +| Partnerportal | 2027, wenn genug Partner aktiv sind | +| Kundenmappe | Konzept wird überarbeitet | +| Shop (Katalog und Verkauf) | Kalkulation liegt vor: 149–235 h | +| Immobilien-CMS | Kostenplan liegt vor, nicht freigegeben | +| Dubai-Map | setzt Immobilien-CMS voraus | +| Marktplätze | spätere Shop-Stufe | +| Bestandskunden-Anschreiben | Idee vorhanden, noch nicht angeboten | +| Markenrichtlinie Fahrzeug und Print | eigene Design-Aufgabe | + +--- + +*Aufwandsschätzung auf Basis des aktuellen Stands, kein Festpreis. Abrechnung nach tatsächlichen, dokumentierten Stunden. Zusätzliche Anforderungen werden vorab abgestimmt und erst nach Freigabe umgesetzt.* \ No newline at end of file diff --git a/dev/2026-08-28/umsetzungliste.md b/dev/2026-08-28/umsetzungliste.md new file mode 100644 index 00000000..2da42e13 --- /dev/null +++ b/dev/2026-08-28/umsetzungliste.md @@ -0,0 +1,120 @@ +# B2in Interior – Umsetzungsliste + +**Freigegebener Rahmen: ca. 37–39 Std.** (30 Basis + 3 Display-Spiegelung + 4–6 Rubrik-Seite) +Schreibweisen: **Interior**, **Febrü**, **Rotpunkt Küchen**, **CABINET** + +Legende: ✅ freigegeben · 🟡 mitnehmen, wenn ohne Mehraufwand machbar · ⏸ später, separat zu beziffern + +--- + +## 1. Website-Umstrukturierung & Umtextung ✅ *(ca. 18 Std.)* + +- Meta-Title und Description auf Interior-Ausrichtung umstellen +- OG-Title / OG-Description anpassen +- Navigation umbauen: „Marken" und „Referenzen" rein, „Immobilien" nach hinten, „Netzwerk" raus aus der Hauptnavi (Seite bleibt erreichbar), FAQ in den Footer +- Ankündigungsleiste neu belegen (Interior-Thema statt Dubai-Launch) +- Hero: neue Headline, Subline, Buttons, Checkmarks, Bild-Badge +- Marcel-Badge unter dem Hero umtexten +- Sektion „Zwei Welten. Ein Netzwerk." ersetzen durch **Weiche Privat / Geschäft** (zwei Karten) +- Sektion „Über Marcel / Vertrauen" umtexten – inkl. Markenstory *Bridges to International / Bridges to Interior* +- Sektion „Ein Ökosystem, drei Säulen" entfernen (Fläche übernimmt Aktuelles/Referenzen) +- Immobilien-Teaser neu anlegen: klein, dezent, kurz vor dem Abschluss +- Abschluss-CTA umtexten, zwei Kontakt-Buttons +- Footer-Claim ersetzen („Connecting Design and Property" → neuer Claim) +- Durchgang auf Alt-Begriffe: „Supply-Chain", „Local-for-Local", „Ökosystem", „Investment" – auch auf /netzwerk, /about, /faq + +*🟡 Logo-Zusatz „Interior": Wenn Marcel keine Datei liefert, typografisch mit vorhandener Schrift ergänzen – kleiner Aufwand, versuche ich mitzunehmen.* + +--- + +## 2. Kernmarken-Modul im CMS ✅ *(ca. 8 Std.)* + +- CMS-Struktur anlegen (adaptiert vom Magazin-Muster: Übersicht + Detailseite) +- Von Marcel selbst erweiterbar, weitere Marken ohne Codeänderung ergänzbar +- Kachel-Übersicht auf der Startseite (`#marken`) +- Drei Marken anlegen: CABINET, Rotpunkt Küchen, Febrü +- Detailseiten-Vorlage: Header, Einleitung, Leistungen, Galerie, CTA-Block + +*Platzhalter, solange Logos und Infos zu Rotpunkt und Febrü fehlen.* + +--- + +## 3. Terminanfrage-Widget ✅ *(ca. 4 Std.)* + +- Schwebendes Element unten rechts, auf allen Seiten +- Felder: Name, E-Mail, Telefon (optional), Anliegen/Marke (Auswahl), Wunschtermin (optional), Nachricht +- Versand per Mail an Marcel, Bestätigungstext für den Absender +- **WhatsApp-Button** daneben: Klick-to-Chat, öffnet direkt den Chat (Handy: App, Desktop: WhatsApp Web) +- *Kein Buchungskalender – bewusst schlank* + +*Offen: WhatsApp-Nummer bestätigen lassen.* + +--- + +## 4. Display-Spiegelung auf der Website ✅ *(ca. 3 Std.)* + +- Bestehende Display-Inhalte (9:16) werden auf der Website ausgespielt +- Nutzt die vorhandene Display-/Player-API als Datenquelle +- Keine doppelte Pflege: Marcel pflegt weiterhin nur im Display-CMS + +*⏸ 16:9-Modul wurde zurückgestellt.* + +--- + +## 5. Rubrik-Seite „Referenzen" (Web-Display) ✅ *(ca. 4–6 Std.)* + +- Zweites, separates Display im Display-CMS anlegen +- Eigene Unterseite `/referenzen`, die diese Inhalte ausspielt +- Von Marcel selbst pflegbar: Bilder, Texte, YouTube-Verlinkungen +- Teaser-Sektion auf der Startseite mit Verlinkung +- Aufnahme in die Navigation +- 2–3 Platzhalter-Einträge, damit der Bereich nicht leer wirkt + +*Inhaltliche Regel: Unter „Referenzen" nur realisierte Projekte. Visualisierungen und Entwürfe gehören in die Kernmarken-Detailseiten.* + +--- + +## 6. Engagement / Sponsoring 🟡 *(Versuch ohne Mehraufwand)* + +- Schlanke Sektion „Regional verbunden" nach der Marcel-Sektion +- Drei gelieferte Logos in ruhiger Reihe, gedeckt oder Graustufen +- Ein Satz Einleitungstext + +*Wird mitgenommen, solange es bei Logo-Leiste plus Satz bleibt. Eigene Unterseite oder Text je Verein wäre ein Zusatz.* + +--- + +## Später separat zu beziffern ⏸ + +| Thema | Warum nicht jetzt | +|---|---| +| **Referenzbereich voll ausgebaut** | Eigene Projektseiten, Kategorien, Galerien – eigenes Modul. Die schlanke Rubrik (Punkt 5) reicht für den Start. | +| **16:9-Display-Modul** | Von Marcel selbst zurückgestellt. Braucht zusätzlich querformat-taugliches Bildmaterial. | +| **Immobilien-CMS** | Immobilien bleiben statisch, rücken nur in der Gewichtung nach hinten. Separater Kostenplan liegt vor. | +| **Dubai-Map** | Interaktive Karte – eigenes Paket, setzt das Immobilien-CMS voraus. | +| **Markenrichtlinie Fahrzeug / Print** | Kombinationsregeln B2in + Kernmarken für Fahrzeugbeschriftung, Visitenkarten, Social Media. Eigene Design-Aufgabe, sinnvoll nach dem Website-Livegang. | +| **Weitere frei anlegbare Rubriken** | Jede zusätzliche Rubrik ist ein eigener kleiner Baustein. | +| **Eigene Videolösung / Player** | Nicht nötig – Videos laufen über YouTube-Einbindung. | + +--- + +## Spielregeln + +- **Eine Korrekturschleife je Bereich** ist enthalten (Abstände, Positionen, Farben, Gewichtungen) +- Weitere Runden oder neue Anforderungen: vorher kurz melden, Mehraufwand nennen, erst nach Freigabe umsetzen +- **Deadline Ende August** – abhängig von rechtzeitigen Zulieferungen + +--- + +## Offene Zulieferungen + +| Was | Status | +|---|---| +| Sponsoring-Logos | ✅ 3 Stück erhalten | +| Logo mit Zusatz „Interior" | offen – ggf. selbst ergänzen | +| Rotpunkt: Logo + Basis-Infos | offen – Platzhalter | +| Febrü: Logo + Basis-Infos | offen – Platzhalter | +| Hero-Bildmotiv (Interior) | offen – Bestandsbild belassen | +| Bilder Kernmarken-Kacheln | offen – Platzhalter | +| CABINET-Referenzprojekte | offen – Platzhalter-Einträge | +| WhatsApp-Nummer | offen – bestätigen lassen | \ No newline at end of file diff --git a/dev/2026-08-28/unterseite_marke.md b/dev/2026-08-28/unterseite_marke.md new file mode 100644 index 00000000..4af5275b --- /dev/null +++ b/dev/2026-08-28/unterseite_marke.md @@ -0,0 +1,130 @@ +# B2in Interior – Unterseite `/marken` + +Vollständige Texte zur Umsetzung. Schreibweisen: **Interior**, **Febrü**, **Rotpunkt Küchen**, **CABINET**. +Platzhalter: `[PH]` + +--- + +## Meta / SEO + +| Feld | Inhalt | +|---|---| +| **Title** | Unsere Marken – CABINET, Rotpunkt Küchen & Febrü \| B2in Interior | +| **Description** | Ausgewählte Hersteller statt beliebiger Kataloge: maßgefertigte Einbauschränke, Premium-Küchen und Möbel fürs Arbeiten – mit persönlicher Beratung in der Region. | +| **URL** | `/marken` | +| **Breadcrumb** | Home → Marken | + +--- + +## 1. Seitenkopf + +> # Unsere Marken +> +> Wir arbeiten nicht mit beliebigen Katalogen, sondern mit ausgewählten Herstellern, hinter denen wir stehen. Jede Marke ist Spezialist auf ihrem Gebiet – und jede bringt mit, was gute Einrichtung ausmacht: Qualität, die man sieht, und Beratung, die zuhört. + +**Bild:** `[PH]` Ruhiges Interior-Motiv oder Showroom-Aufnahme. Alternativ ohne Headerbild, dafür großzügiger Weißraum. + +--- + +## 2. Kernmarken + +*Reihenfolge wie auf der Startseite. Pro Marke: Logo, Bild, Claim, Beschreibung, Merkmale, Link zur Detailseite.* + +### CABINET + +> ### CABINET +> **Einbauschränke nach Maß.** +> +> Wo Standardmaße an ihre Grenzen kommen, fängt CABINET an. Schranklösungen, die exakt in Ihren Raum geplant werden – von der Dachschräge über die Nische bis zur raumhohen Front. Gefertigt in der Qualität, die man erst beim zweiten Hinschauen ganz erfasst. +> +> Erleben Sie die Möglichkeiten in unserem Showroom in Bielefeld. +> +> - Millimetergenaue Planung +> - Individuelle Fronten und Innenausstattung +> - Persönliche Beratung vor Ort +> +> **Mehr zu CABINET →** + +### Rotpunkt Küchen + +> ### Rotpunkt Küchen +> **Küchen, die Haltung zeigen.** +> +> Premium-Küchen aus deutscher Fertigung – individuell geplant und gebaut für den Raum, in dem am meisten passiert. Rotpunkt steht für durchdachte Funktion, langlebige Materialien und einen Anspruch an Nachhaltigkeit, der über das Marketing hinausgeht. +> +> Beratung und Planung in unserem Küchenstudio. +> +> - Fertigung in Deutschland +> - Individuelle Planung nach Ihrem Grundriss +> - Nachhaltige Materialien +> +> **Mehr zu Rotpunkt →** + +### Febrü + +> ### Febrü +> **Arbeiten, wo man gerne ist.** +> +> Ob Home-Office oder ganze Büroetage: Febrü macht Möbel für konzentriertes Arbeiten. Höhenverstellbare Tische, durchdachte Stauraumlösungen und Sitzmöbel, die auch nach acht Stunden noch überzeugen – mit einem Design, das ins Zuhause genauso passt wie ins Unternehmen. +> +> - Ergonomie, die man merkt +> - Vom einzelnen Arbeitsplatz bis zur Bürofläche +> - Für Zuhause und fürs Unternehmen +> +> **Mehr zu Febrü →** + +`[PH]` Logos und Bildmaterial zu Rotpunkt und Febrü fehlen – Platzhalter setzen. + +--- + +## 3. Partnernetzwerk + +*Nimmt den Netzwerk-Gedanken der alten Seite auf, in kundenverständlicher Sprache.* + +> ## Mehr als Marken: unser Netzwerk +> +> Gute Einrichtung endet nicht beim Möbelstück. Deshalb arbeiten wir mit Fachbetrieben und Handwerkern aus der Region zusammen – von der Aufmaß-Erstellung über die Montage bis zu angrenzenden Gewerken. +> +> Für Sie bedeutet das: ein Ansprechpartner, der die Fäden zusammenhält, statt vieler Einzelgespräche. Und Partner, die wir kennen und für deren Arbeit wir geradestehen. + +**Optionale drei Punkte darunter:** + +> **Aufmaß & Planung** +> Wir kommen zu Ihnen, nehmen Maß und planen mit Ihnen gemeinsam. +> +> **Montage durch Fachbetriebe** +> Handwerker aus der Region, die wir persönlich kennen. +> +> **Ein Ansprechpartner** +> Von der ersten Idee bis zur Übergabe koordiniert aus einer Hand. + +--- + +## 4. Marken werden ergänzt + +*Kurzer Block, der die Erweiterbarkeit signalisiert. Optional – nur einbauen, wenn die Seite sonst zu kurz wirkt.* + +> ## Unsere Markenwelt wächst +> +> Wir erweitern unser Portfolio laufend um Hersteller, die zu unserem Anspruch passen. Sie vermissen eine bestimmte Marke oder haben einen konkreten Wunsch? Sprechen Sie uns an – oft finden wir über unser Netzwerk genau das Richtige. + +--- + +## 5. Abschluss-CTA + +> ## Lernen Sie unsere Marken kennen +> +> Am besten dort, wo Sie sie anfassen können: in unseren Showrooms. Vereinbaren Sie einen unverbindlichen Beratungstermin – wir nehmen uns Zeit. +> +> **Button 1:** Termin anfragen +> **Button 2:** Per WhatsApp schreiben + +--- + +## 6. Umsetzungshinweise + +- **Startseiten-Sektion bleibt** als Teaser mit verkürzten Texten. Kacheln verlinken direkt auf die jeweilige Detailseite, darunter ein Button **„Alle Marken ansehen"** → `/marken`. +- **Navigation:** „Marken" verlinkt auf `/marken`, nicht mehr per Anker auf die Startseite. +- **Detailseiten** hängen unter `/marken/cabinet`, `/marken/rotpunkt`, `/marken/februe` – dadurch saubere Hierarchie und funktionierender Rückweg. +- **Alte `/netzwerk`-Seite:** Inhalt geht in Abschnitt 3 auf. Seite entfernen und per 301 auf `/marken` weiterleiten, damit bestehende Links und Rankings nicht ins Leere laufen. +- **Reihenfolge der Marken** über das CMS steuerbar halten, damit Marcel sie später selbst ändern kann. \ No newline at end of file diff --git a/public/_cabinet/_docs/b2in-displays.md b/public/_cabinet/_docs/b2in-displays.md index b818d869..8279c214 100644 --- a/public/_cabinet/_docs/b2in-displays.md +++ b/public/_cabinet/_docs/b2in-displays.md @@ -90,7 +90,7 @@ Jedes Playlist-Item hat folgende Eigenschaften: | `media_url` | URL (optional) | Alternative zu Upload: Externe Video-/Bild-URL. Wird bevorzugt wenn vorhanden. | | `headline` | Text | Max. 40 Zeichen. Wird im Textfeld angezeigt. | | `subline` | Text | Max. 80 Zeichen. Erklärender Text unter der Headline. | -| `duration_seconds` | Zahl | Anzeigedauer in Sekunden. Für Bilder: empfohlen 8–12 Sek. Für Videos: wird ignoriert, Dauer ergibt sich aus Videolänge. | +| `duration_seconds` | Zahl | Anzeigedauer in Sekunden, auch mit Nachkommastelle (z. B. `1.5`). Für Bilder: empfohlen 8–12 Sek. Für Videos: wird ignoriert, Dauer ergibt sich aus Videolänge. | | `sort_order` | Zahl | Reihenfolge in der Playlist. Niedrigste Zahl zuerst. | | `is_active` | Toggle | Aktiv/Inaktiv. Inaktive Items werden übersprungen. | diff --git a/public/_cabinet/assets/b2in-interior-logo-negative.svg b/public/_cabinet/assets/b2in-interior-logo-negative.svg new file mode 100644 index 00000000..838bf931 --- /dev/null +++ b/public/_cabinet/assets/b2in-interior-logo-negative.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_cabinet/assets/b2in-interior-logo-positive.svg b/public/_cabinet/assets/b2in-interior-logo-positive.svg new file mode 100644 index 00000000..23719451 --- /dev/null +++ b/public/_cabinet/assets/b2in-interior-logo-positive.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_cabinet/assets/b2in-logo-positive.svg b/public/_cabinet/assets/b2in-logo-positive.svg index 58aae48e..dabb1b97 100644 --- a/public/_cabinet/assets/b2in-logo-positive.svg +++ b/public/_cabinet/assets/b2in-logo-positive.svg @@ -1,21 +1,36 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/public/_cabinet/display/index.html b/public/_cabinet/display/index.html index eee68e97..acc7d0d2 100644 --- a/public/_cabinet/display/index.html +++ b/public/_cabinet/display/index.html @@ -145,7 +145,7 @@ /* B2in Brand (positionable logo + claim) */ .b2in-brand { position: absolute; z-index: 12; display: flex; align-items: center; max-width: 60%; } - .b2in-brand-logo img { height: 3.5vh; display: block; filter: drop-shadow(0 1px 4px rgba(0,0,0,0.45)); } + .b2in-brand-logo img { height: 4.6vh; display: block; filter: drop-shadow(0 1px 4px rgba(0,0,0,0.45)); } .b2in-brand-claim { font-size: 1.3vh; font-weight: 300; letter-spacing: 0.15em; text-transform: uppercase; color: rgba(255,255,255,0.85); @@ -1337,7 +1337,7 @@ class B2inRenderer { layer.className = 'version-layer b2in-layer active' + (this.settings.show_footer === false ? ' no-footer' : ''); layer.setAttribute('data-theme', this.theme); - const headerLogoUrl = this.resolveUrl(this.settings.header_logo_url || '/_cabinet/assets/b2in-logo-positive.svg'); + const headerLogoUrl = this.resolveUrl(this.settings.header_logo_url || '/_cabinet/assets/b2in-interior-logo-negative.svg'); const headerClaim = this.settings.header_claim || 'Connecting Design & Property'; const footerUrl = this.settings.footer_url || 'B2in.eu'; const footerName = this.settings.footer_name || ''; diff --git a/public/favicon/android-icon-144x144.png b/public/favicon/android-icon-144x144.png index 4d59c540..52126729 100644 Binary files a/public/favicon/android-icon-144x144.png and b/public/favicon/android-icon-144x144.png differ diff --git a/public/favicon/android-icon-192x192.png b/public/favicon/android-icon-192x192.png index 338c769a..5db2bf34 100644 Binary files a/public/favicon/android-icon-192x192.png and b/public/favicon/android-icon-192x192.png differ diff --git a/public/favicon/android-icon-36x36.png b/public/favicon/android-icon-36x36.png index 31171287..9711cc1f 100644 Binary files a/public/favicon/android-icon-36x36.png and b/public/favicon/android-icon-36x36.png differ diff --git a/public/favicon/android-icon-48x48.png b/public/favicon/android-icon-48x48.png index 9fe5826b..855c2c14 100644 Binary files a/public/favicon/android-icon-48x48.png and b/public/favicon/android-icon-48x48.png differ diff --git a/public/favicon/android-icon-72x72.png b/public/favicon/android-icon-72x72.png index 94f262e2..2a321e28 100644 Binary files a/public/favicon/android-icon-72x72.png and b/public/favicon/android-icon-72x72.png differ diff --git a/public/favicon/android-icon-96x96.png b/public/favicon/android-icon-96x96.png index f3afd5d5..68676dd8 100644 Binary files a/public/favicon/android-icon-96x96.png and b/public/favicon/android-icon-96x96.png differ diff --git a/public/favicon/apple-icon-114x114.png b/public/favicon/apple-icon-114x114.png index 0f5c644e..76bfc7aa 100644 Binary files a/public/favicon/apple-icon-114x114.png and b/public/favicon/apple-icon-114x114.png differ diff --git a/public/favicon/apple-icon-120x120.png b/public/favicon/apple-icon-120x120.png index e1e9b4d3..201c1b6b 100644 Binary files a/public/favicon/apple-icon-120x120.png and b/public/favicon/apple-icon-120x120.png differ diff --git a/public/favicon/apple-icon-144x144.png b/public/favicon/apple-icon-144x144.png index f81b3bf3..3019c5e1 100644 Binary files a/public/favicon/apple-icon-144x144.png and b/public/favicon/apple-icon-144x144.png differ diff --git a/public/favicon/apple-icon-152x152.png b/public/favicon/apple-icon-152x152.png index b3ce7178..999c4aa1 100644 Binary files a/public/favicon/apple-icon-152x152.png and b/public/favicon/apple-icon-152x152.png differ diff --git a/public/favicon/apple-icon-180x180.png b/public/favicon/apple-icon-180x180.png index acf55a81..d0e44c56 100644 Binary files a/public/favicon/apple-icon-180x180.png and b/public/favicon/apple-icon-180x180.png differ diff --git a/public/favicon/apple-icon-57x57.png b/public/favicon/apple-icon-57x57.png index bd69e12b..d0812f1f 100644 Binary files a/public/favicon/apple-icon-57x57.png and b/public/favicon/apple-icon-57x57.png differ diff --git a/public/favicon/apple-icon-60x60.png b/public/favicon/apple-icon-60x60.png index 8ab2dba2..5e527983 100644 Binary files a/public/favicon/apple-icon-60x60.png and b/public/favicon/apple-icon-60x60.png differ diff --git a/public/favicon/apple-icon-72x72.png b/public/favicon/apple-icon-72x72.png index 94f262e2..334084ed 100644 Binary files a/public/favicon/apple-icon-72x72.png and b/public/favicon/apple-icon-72x72.png differ diff --git a/public/favicon/apple-icon-76x76.png b/public/favicon/apple-icon-76x76.png index f79c946a..b04b7055 100644 Binary files a/public/favicon/apple-icon-76x76.png and b/public/favicon/apple-icon-76x76.png differ diff --git a/public/favicon/apple-icon-precomposed.png b/public/favicon/apple-icon-precomposed.png index 3b0de1af..87337674 100644 Binary files a/public/favicon/apple-icon-precomposed.png and b/public/favicon/apple-icon-precomposed.png differ diff --git a/public/favicon/apple-icon.png b/public/favicon/apple-icon.png index 3b0de1af..87337674 100644 Binary files a/public/favicon/apple-icon.png and b/public/favicon/apple-icon.png differ diff --git a/public/favicon/favicon-16x16.png b/public/favicon/favicon-16x16.png index 6f20373d..d12ce017 100644 Binary files a/public/favicon/favicon-16x16.png and b/public/favicon/favicon-16x16.png differ diff --git a/public/favicon/favicon-32x32.png b/public/favicon/favicon-32x32.png index 7bc484a1..126c2ab3 100644 Binary files a/public/favicon/favicon-32x32.png and b/public/favicon/favicon-32x32.png differ diff --git a/public/favicon/favicon-96x96.png b/public/favicon/favicon-96x96.png index 2140f245..68676dd8 100644 Binary files a/public/favicon/favicon-96x96.png and b/public/favicon/favicon-96x96.png differ diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico index c949cadb..11bdea25 100644 Binary files a/public/favicon/favicon.ico and b/public/favicon/favicon.ico differ diff --git a/public/favicon/ms-icon-144x144.png b/public/favicon/ms-icon-144x144.png index f81b3bf3..52126729 100644 Binary files a/public/favicon/ms-icon-144x144.png and b/public/favicon/ms-icon-144x144.png differ diff --git a/public/favicon/ms-icon-150x150.png b/public/favicon/ms-icon-150x150.png index 5720d210..f0255e1c 100644 Binary files a/public/favicon/ms-icon-150x150.png and b/public/favicon/ms-icon-150x150.png differ diff --git a/public/favicon/ms-icon-310x310.png b/public/favicon/ms-icon-310x310.png index 4ba01825..dd42c76d 100644 Binary files a/public/favicon/ms-icon-310x310.png and b/public/favicon/ms-icon-310x310.png differ diff --git a/public/favicon/ms-icon-70x70.png b/public/favicon/ms-icon-70x70.png index a242a181..13d3fbe0 100644 Binary files a/public/favicon/ms-icon-70x70.png and b/public/favicon/ms-icon-70x70.png differ diff --git a/public/img/assets/b2in/marcel-scheibe.jpg b/public/img/assets/b2in/marcel-scheibe.jpg index 6ba9c975..73842153 100644 Binary files a/public/img/assets/b2in/marcel-scheibe.jpg and b/public/img/assets/b2in/marcel-scheibe.jpg differ diff --git a/public/img/assets/b2in/marcel-scheibe.webp b/public/img/assets/b2in/marcel-scheibe.webp deleted file mode 100644 index 2c91d724..00000000 Binary files a/public/img/assets/b2in/marcel-scheibe.webp and /dev/null differ diff --git a/public/img/assets/marken/cabinet.svg b/public/img/assets/marken/cabinet.svg new file mode 100644 index 00000000..79dd9b8e --- /dev/null +++ b/public/img/assets/marken/cabinet.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano-seite.webp b/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano-seite.webp new file mode 100644 index 00000000..54903ff6 Binary files /dev/null and b/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano-seite.webp differ diff --git a/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano.webp b/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano.webp new file mode 100644 index 00000000..020e9d35 Binary files /dev/null and b/public/img/assets/marken/cabinet/ankleide-solitaer-eiche-tano.webp differ diff --git a/public/img/assets/marken/cabinet/dachschraege-velluto-fango.webp b/public/img/assets/marken/cabinet/dachschraege-velluto-fango.webp new file mode 100644 index 00000000..80de2c69 Binary files /dev/null and b/public/img/assets/marken/cabinet/dachschraege-velluto-fango.webp differ diff --git a/public/img/assets/marken/cabinet/dachschraegenschrank-filo-cashmere.webp b/public/img/assets/marken/cabinet/dachschraegenschrank-filo-cashmere.webp new file mode 100644 index 00000000..f257f36d Binary files /dev/null and b/public/img/assets/marken/cabinet/dachschraegenschrank-filo-cashmere.webp differ diff --git a/public/img/assets/marken/cabinet/drehtuerenschrank-marmor.webp b/public/img/assets/marken/cabinet/drehtuerenschrank-marmor.webp new file mode 100644 index 00000000..30114cdf Binary files /dev/null and b/public/img/assets/marken/cabinet/drehtuerenschrank-marmor.webp differ diff --git a/public/img/assets/marken/cabinet/flur-schrank-eiche-tano.webp b/public/img/assets/marken/cabinet/flur-schrank-eiche-tano.webp new file mode 100644 index 00000000..8112d4ef Binary files /dev/null and b/public/img/assets/marken/cabinet/flur-schrank-eiche-tano.webp differ diff --git a/public/img/assets/marken/cabinet/garderobe-seite-offen.webp b/public/img/assets/marken/cabinet/garderobe-seite-offen.webp new file mode 100644 index 00000000..7f81cf4a Binary files /dev/null and b/public/img/assets/marken/cabinet/garderobe-seite-offen.webp differ diff --git a/public/img/assets/marken/cabinet/garderobe-sitznische-filo-cashmere.webp b/public/img/assets/marken/cabinet/garderobe-sitznische-filo-cashmere.webp new file mode 100644 index 00000000..bd480515 Binary files /dev/null and b/public/img/assets/marken/cabinet/garderobe-sitznische-filo-cashmere.webp differ diff --git a/public/img/assets/marken/cabinet/schlafzimmer-eiche-tano.webp b/public/img/assets/marken/cabinet/schlafzimmer-eiche-tano.webp new file mode 100644 index 00000000..a607e8f0 Binary files /dev/null and b/public/img/assets/marken/cabinet/schlafzimmer-eiche-tano.webp differ diff --git a/public/img/assets/marken/cabinet/schlafzimmer-velluto-fango.webp b/public/img/assets/marken/cabinet/schlafzimmer-velluto-fango.webp new file mode 100644 index 00000000..905f1af8 Binary files /dev/null and b/public/img/assets/marken/cabinet/schlafzimmer-velluto-fango.webp differ diff --git a/public/img/assets/marken/cabinet/schlafzimmer-wandschrank-filo-cashmere.webp b/public/img/assets/marken/cabinet/schlafzimmer-wandschrank-filo-cashmere.webp new file mode 100644 index 00000000..d5b9db59 Binary files /dev/null and b/public/img/assets/marken/cabinet/schlafzimmer-wandschrank-filo-cashmere.webp differ diff --git a/public/img/assets/marken/cabinet/schrank-gegen-schraege-marmor.webp b/public/img/assets/marken/cabinet/schrank-gegen-schraege-marmor.webp new file mode 100644 index 00000000..b6d345c5 Binary files /dev/null and b/public/img/assets/marken/cabinet/schrank-gegen-schraege-marmor.webp differ diff --git a/public/img/assets/marken/februe.png b/public/img/assets/marken/februe.png new file mode 100644 index 00000000..7651548d Binary files /dev/null and b/public/img/assets/marken/februe.png differ diff --git a/public/img/assets/marken/februe/active-2-0-bureo.webp b/public/img/assets/marken/februe/active-2-0-bureo.webp new file mode 100644 index 00000000..efe9a534 Binary files /dev/null and b/public/img/assets/marken/februe/active-2-0-bureo.webp differ diff --git a/public/img/assets/marken/februe/active-2-0-soul-grossraum.webp b/public/img/assets/marken/februe/active-2-0-soul-grossraum.webp new file mode 100644 index 00000000..568b194e Binary files /dev/null and b/public/img/assets/marken/februe/active-2-0-soul-grossraum.webp differ diff --git a/public/img/assets/marken/februe/adesso.webp b/public/img/assets/marken/februe/adesso.webp new file mode 100644 index 00000000..a107148b Binary files /dev/null and b/public/img/assets/marken/februe/adesso.webp differ diff --git a/public/img/assets/marken/februe/arena-stage.webp b/public/img/assets/marken/februe/arena-stage.webp new file mode 100644 index 00000000..cd9497c4 Binary files /dev/null and b/public/img/assets/marken/februe/arena-stage.webp differ diff --git a/public/img/assets/marken/februe/base-sideboard.webp b/public/img/assets/marken/februe/base-sideboard.webp new file mode 100644 index 00000000..4be54371 Binary files /dev/null and b/public/img/assets/marken/februe/base-sideboard.webp differ diff --git a/public/img/assets/marken/februe/base-thekenloesung.webp b/public/img/assets/marken/februe/base-thekenloesung.webp new file mode 100644 index 00000000..55a5053b Binary files /dev/null and b/public/img/assets/marken/februe/base-thekenloesung.webp differ diff --git a/public/img/assets/marken/februe/base.webp b/public/img/assets/marken/februe/base.webp new file mode 100644 index 00000000..9f760baa Binary files /dev/null and b/public/img/assets/marken/februe/base.webp differ diff --git a/public/img/assets/marken/februe/bobb-bionics-coworker-container-cube.webp b/public/img/assets/marken/februe/bobb-bionics-coworker-container-cube.webp new file mode 100644 index 00000000..3ea85951 Binary files /dev/null and b/public/img/assets/marken/februe/bobb-bionics-coworker-container-cube.webp differ diff --git a/public/img/assets/marken/februe/bobb.webp b/public/img/assets/marken/februe/bobb.webp new file mode 100644 index 00000000..68edfdae Binary files /dev/null and b/public/img/assets/marken/februe/bobb.webp differ diff --git a/public/img/assets/marken/februe/brandon.webp b/public/img/assets/marken/februe/brandon.webp new file mode 100644 index 00000000..28c9826c Binary files /dev/null and b/public/img/assets/marken/februe/brandon.webp differ diff --git a/public/img/assets/marken/februe/corporate.webp b/public/img/assets/marken/februe/corporate.webp new file mode 100644 index 00000000..370d92f5 Binary files /dev/null and b/public/img/assets/marken/februe/corporate.webp differ diff --git a/public/img/assets/marken/februe/coworker.webp b/public/img/assets/marken/februe/coworker.webp new file mode 100644 index 00000000..e5f9db17 Binary files /dev/null and b/public/img/assets/marken/februe/coworker.webp differ diff --git a/public/img/assets/marken/februe/curved-ben-basic.webp b/public/img/assets/marken/februe/curved-ben-basic.webp new file mode 100644 index 00000000..74d1908c Binary files /dev/null and b/public/img/assets/marken/februe/curved-ben-basic.webp differ diff --git a/public/img/assets/marken/februe/elements-soul-grossraum.webp b/public/img/assets/marken/februe/elements-soul-grossraum.webp new file mode 100644 index 00000000..99e62efc Binary files /dev/null and b/public/img/assets/marken/februe/elements-soul-grossraum.webp differ diff --git a/public/img/assets/marken/februe/eline-one-silent-garderobenpaneel.webp b/public/img/assets/marken/februe/eline-one-silent-garderobenpaneel.webp new file mode 100644 index 00000000..ba010f49 Binary files /dev/null and b/public/img/assets/marken/februe/eline-one-silent-garderobenpaneel.webp differ diff --git a/public/img/assets/marken/februe/vio.webp b/public/img/assets/marken/februe/vio.webp new file mode 100644 index 00000000..982abb37 Binary files /dev/null and b/public/img/assets/marken/februe/vio.webp differ diff --git a/public/img/assets/marken/otterpohl.png b/public/img/assets/marken/otterpohl.png new file mode 100644 index 00000000..a7b0aea9 Binary files /dev/null and b/public/img/assets/marken/otterpohl.png differ diff --git a/public/img/assets/marken/otterpohl/referenz-berkemeier-treppe.webp b/public/img/assets/marken/otterpohl/referenz-berkemeier-treppe.webp new file mode 100644 index 00000000..b77a910c Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-berkemeier-treppe.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-dziuballe-treppe.webp b/public/img/assets/marken/otterpohl/referenz-dziuballe-treppe.webp new file mode 100644 index 00000000..d57c29e0 Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-dziuballe-treppe.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-luecke-01.webp b/public/img/assets/marken/otterpohl/referenz-luecke-01.webp new file mode 100644 index 00000000..ebae4d4b Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-luecke-01.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-luecke-12.webp b/public/img/assets/marken/otterpohl/referenz-luecke-12.webp new file mode 100644 index 00000000..ef5d905d Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-luecke-12.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-luecke-15.webp b/public/img/assets/marken/otterpohl/referenz-luecke-15.webp new file mode 100644 index 00000000..bed7d61f Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-luecke-15.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-neugebauer.webp b/public/img/assets/marken/otterpohl/referenz-neugebauer.webp new file mode 100644 index 00000000..405851c3 Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-neugebauer.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-objektbau-luecke-technik.webp b/public/img/assets/marken/otterpohl/referenz-objektbau-luecke-technik.webp new file mode 100644 index 00000000..43b257ce Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-objektbau-luecke-technik.webp differ diff --git a/public/img/assets/marken/otterpohl/referenz-otterpohl-11.webp b/public/img/assets/marken/otterpohl/referenz-otterpohl-11.webp new file mode 100644 index 00000000..157a27ef Binary files /dev/null and b/public/img/assets/marken/otterpohl/referenz-otterpohl-11.webp differ diff --git a/public/img/assets/marken/otterpohl/werkstatt-02.webp b/public/img/assets/marken/otterpohl/werkstatt-02.webp new file mode 100644 index 00000000..8eba27be Binary files /dev/null and b/public/img/assets/marken/otterpohl/werkstatt-02.webp differ diff --git a/public/img/assets/marken/otterpohl/werkstatt-03.webp b/public/img/assets/marken/otterpohl/werkstatt-03.webp new file mode 100644 index 00000000..2e5d2611 Binary files /dev/null and b/public/img/assets/marken/otterpohl/werkstatt-03.webp differ diff --git a/public/img/assets/marken/otterpohl/werkstatt-04.webp b/public/img/assets/marken/otterpohl/werkstatt-04.webp new file mode 100644 index 00000000..faf65a8a Binary files /dev/null and b/public/img/assets/marken/otterpohl/werkstatt-04.webp differ diff --git a/public/img/assets/marken/otterpohl/werkstatt-05-lift.webp b/public/img/assets/marken/otterpohl/werkstatt-05-lift.webp new file mode 100644 index 00000000..dbe28bff Binary files /dev/null and b/public/img/assets/marken/otterpohl/werkstatt-05-lift.webp differ diff --git a/public/img/assets/marken/otterpohl/werkstatt-titel.webp b/public/img/assets/marken/otterpohl/werkstatt-titel.webp new file mode 100644 index 00000000..3efdf8db Binary files /dev/null and b/public/img/assets/marken/otterpohl/werkstatt-titel.webp differ diff --git a/public/img/assets/marken/rotpunkt.svg b/public/img/assets/marken/rotpunkt.svg new file mode 100644 index 00000000..d66ae475 --- /dev/null +++ b/public/img/assets/marken/rotpunkt.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/assets/marken/rotpunkt/kueche-01-b.webp b/public/img/assets/marken/rotpunkt/kueche-01-b.webp new file mode 100644 index 00000000..b66eaedf Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-01-b.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-01.webp b/public/img/assets/marken/rotpunkt/kueche-01.webp new file mode 100644 index 00000000..4e244ee3 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-01.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-02.webp b/public/img/assets/marken/rotpunkt/kueche-02.webp new file mode 100644 index 00000000..864706ea Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-02.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-03.webp b/public/img/assets/marken/rotpunkt/kueche-03.webp new file mode 100644 index 00000000..e8a90376 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-03.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-04.webp b/public/img/assets/marken/rotpunkt/kueche-04.webp new file mode 100644 index 00000000..dfb78e7e Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-04.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-05.webp b/public/img/assets/marken/rotpunkt/kueche-05.webp new file mode 100644 index 00000000..e78835a4 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-05.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-06-b.webp b/public/img/assets/marken/rotpunkt/kueche-06-b.webp new file mode 100644 index 00000000..1ee9f169 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-06-b.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-06.webp b/public/img/assets/marken/rotpunkt/kueche-06.webp new file mode 100644 index 00000000..73b74d11 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-06.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-07.webp b/public/img/assets/marken/rotpunkt/kueche-07.webp new file mode 100644 index 00000000..abaf5e75 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-07.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-08.webp b/public/img/assets/marken/rotpunkt/kueche-08.webp new file mode 100644 index 00000000..e587704f Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-08.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-09.webp b/public/img/assets/marken/rotpunkt/kueche-09.webp new file mode 100644 index 00000000..82a658ce Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-09.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-10.webp b/public/img/assets/marken/rotpunkt/kueche-10.webp new file mode 100644 index 00000000..bd62678d Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-10.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-11.webp b/public/img/assets/marken/rotpunkt/kueche-11.webp new file mode 100644 index 00000000..79d6bbd2 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-11.webp differ diff --git a/public/img/assets/marken/rotpunkt/kueche-12.webp b/public/img/assets/marken/rotpunkt/kueche-12.webp new file mode 100644 index 00000000..6b611a79 Binary files /dev/null and b/public/img/assets/marken/rotpunkt/kueche-12.webp differ diff --git a/public/img/assets/sponsoring/arminia-bielefeld.png b/public/img/assets/sponsoring/arminia-bielefeld.png new file mode 100644 index 00000000..2b05fe31 Binary files /dev/null and b/public/img/assets/sponsoring/arminia-bielefeld.png differ diff --git a/public/img/assets/sponsoring/tc-bad-waldliesborn-horn.jpg b/public/img/assets/sponsoring/tc-bad-waldliesborn-horn.jpg new file mode 100644 index 00000000..2609544d Binary files /dev/null and b/public/img/assets/sponsoring/tc-bad-waldliesborn-horn.jpg differ diff --git a/public/img/assets/sponsoring/tc-hoevelhof.png b/public/img/assets/sponsoring/tc-hoevelhof.png new file mode 100644 index 00000000..933ff181 Binary files /dev/null and b/public/img/assets/sponsoring/tc-hoevelhof.png differ diff --git a/public/img/favicons/b2in-favicon.ico b/public/img/favicons/b2in-favicon.ico new file mode 100644 index 00000000..11bdea25 Binary files /dev/null and b/public/img/favicons/b2in-favicon.ico differ diff --git a/public/img/logos/b2in-interior-logo-gold-negative.svg b/public/img/logos/b2in-interior-logo-gold-negative.svg new file mode 100644 index 00000000..0b5009b0 --- /dev/null +++ b/public/img/logos/b2in-interior-logo-gold-negative.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logos/b2in-interior-logo-gold-positive.svg b/public/img/logos/b2in-interior-logo-gold-positive.svg new file mode 100644 index 00000000..b22bc61b --- /dev/null +++ b/public/img/logos/b2in-interior-logo-gold-positive.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logos/b2in-interior-logo-negative.png b/public/img/logos/b2in-interior-logo-negative.png new file mode 100644 index 00000000..f047233c Binary files /dev/null and b/public/img/logos/b2in-interior-logo-negative.png differ diff --git a/public/img/logos/b2in-interior-logo-negative.svg b/public/img/logos/b2in-interior-logo-negative.svg new file mode 100644 index 00000000..838bf931 --- /dev/null +++ b/public/img/logos/b2in-interior-logo-negative.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logos/b2in-interior-logo-positive.png b/public/img/logos/b2in-interior-logo-positive.png new file mode 100644 index 00000000..cb6eadb9 Binary files /dev/null and b/public/img/logos/b2in-interior-logo-positive.png differ diff --git a/public/img/logos/b2in-interior-logo-positive.svg b/public/img/logos/b2in-interior-logo-positive.svg new file mode 100644 index 00000000..23719451 --- /dev/null +++ b/public/img/logos/b2in-interior-logo-positive.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logos/b2in-logo-negative.png b/public/img/logos/b2in-logo-negative.png index 7646afc3..1b846baa 100644 Binary files a/public/img/logos/b2in-logo-negative.png and b/public/img/logos/b2in-logo-negative.png differ diff --git a/public/img/logos/b2in-logo-negative.svg b/public/img/logos/b2in-logo-negative.svg index 6ef5d925..36ffe00b 100644 --- a/public/img/logos/b2in-logo-negative.svg +++ b/public/img/logos/b2in-logo-negative.svg @@ -1,25 +1,36 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logos/b2in-logo-positive.png b/public/img/logos/b2in-logo-positive.png index 20ce0578..731fc145 100644 Binary files a/public/img/logos/b2in-logo-positive.png and b/public/img/logos/b2in-logo-positive.png differ diff --git a/public/img/logos/b2in-logo-positive.svg b/public/img/logos/b2in-logo-positive.svg index fc43ca1e..dabb1b97 100644 --- a/public/img/logos/b2in-logo-positive.svg +++ b/public/img/logos/b2in-logo-positive.svg @@ -1,25 +1,36 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/resources/css/web/shared-styles.css b/resources/css/web/shared-styles.css index ca2c9996..62ebd923 100644 --- a/resources/css/web/shared-styles.css +++ b/resources/css/web/shared-styles.css @@ -232,6 +232,61 @@ h1, h2, h3, h4, h5, h6 { box-shadow: var(--shadow-accent-glow); } +/* Formularfelder (Terminanfrage-Widget, Kontaktseite). Tokenbasiert, damit sie in jeder + Welt zum Grund passen; Fokus im Akzent der Welt. `.form-select` ergaenzt einen eigenen + Chevron, weil der Browser-Pfeil neben den uebrigen Feldern fremd wirkt. */ +.form-field { + display: block; + width: 100%; + padding: 0.65rem 0.9rem; + font-size: 0.95rem; + line-height: 1.5; + color: hsl(var(--foreground)); + background-color: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.6rem; + box-shadow: inset 0 1px 2px hsl(var(--foreground) / 0.04); + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.form-field::placeholder { + color: hsl(var(--muted-foreground) / 0.7); +} + +.form-field:hover { + border-color: hsl(var(--primary) / 0.45); +} + +.form-field:focus, +.form-field:focus-visible { + outline: none; + border-color: hsl(var(--secondary)); + box-shadow: 0 0 0 3px hsl(var(--secondary) / 0.22); + background-color: hsl(var(--background)); +} + +.form-select { + appearance: none; + -webkit-appearance: none; + padding-right: 2.6rem; + cursor: pointer; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 0.9rem center; + background-size: 1.1rem 1.1rem; +} + +.form-select optgroup { + font-weight: 600; + font-style: normal; + color: hsl(var(--primary)); +} + +.form-select option { + font-weight: 400; + color: hsl(var(--foreground)); +} + /* Card styles */ .card-elevated { background-color: hsl(var(--card)); @@ -258,10 +313,94 @@ h1, h2, h3, h4, h5, h6 { color: hsl(var(--dark-text)); } +/* Abschlussband der Marken- und Startseite. + Ein durchgehender Verlauf ueber die volle Breite: vom dunklen Hauptton in den + Akzent. Bewusst ohne weiche Kreise – die setzten sichtbare Flecken in die + Ecken. Die Tiefe kommt aus dem Verlauf selbst, die Kontur aus zwei Haarlinien. + Wo das Band ausläuft, sagt `--cta-band-end` (Interior: tiefes Ocker, + Immobilien: Dynamic Blue); Themes ohne den Token nutzen `--secondary-dark`. */ +.cta-band { + --cta-band-gradient: linear-gradient( + 100deg, + hsl(var(--primary-dark)) 0%, + hsl(var(--primary)) 42%, + hsl(var(--cta-band-end, var(--secondary-dark))) 100% + ); + + position: relative; + overflow: hidden; + background: var(--cta-band-gradient); +} + +/* Lichtkante oben, die zu beiden Seiten auslaeuft (Interior: in Ocker, sonst weiss). */ +.cta-band::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + hsl(var(--cta-band-line, 0 0% 100%) / 0.6) 50%, + transparent 100% + ); +} + +/* Hervorgehobene Woerter im Titel des Bands. Die Inhalte tragen `text-primary` + (dunkler Hauptton) – auf dem dunklen Band unlesbar, deshalb hier der Akzent. */ +.cta-band .text-primary { + color: hsl(var(--secondary)); +} + +/* Umgekehrt auf Flaechen im Akzentton (z. B. „Lernen Sie unsere Marken kennen" auf /marken): + ein `text-secondary`-Wort waere Ocker auf Ocker. Dort traegt es den Hauptton. */ +section.bg-secondary .text-secondary { + color: hsl(var(--primary)); +} + +/* Sehr flacher Schimmer von oben links, damit die Flaeche nicht flach wirkt. */ +.cta-band::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient( + 165deg, + hsl(0 0% 100% / 0.07) 0%, + transparent 48% + ); +} + .text-dark-muted { color: hsl(var(--dark-muted)); } +/* Footer. Eigene Tokens, weil der Footer nicht in jeder Welt dunkel ist: + Interior legt ihn auf Sand mit Tanne, Immobilien auf Anthrazit mit Weiss. + Themes ohne Footer-Tokens fallen auf die dunkle Sektion zurueck. */ +.section-footer { + background-color: hsl(var(--footer-bg, var(--dark-bg))); + color: hsl(var(--footer-fg, var(--dark-text))); +} + +.text-footer-muted { + color: hsl(var(--footer-muted, var(--dark-muted))); +} + +.border-footer-line\/30 { + border-color: hsl(var(--footer-line, var(--dark-muted)) / 0.3); +} + +.text-clay { + color: hsl(var(--clay, var(--muted-foreground))); +} + +.hover-bg-primary-light:hover { + background-color: hsl(var(--primary-light)); +} + /* Container styles */ .container-padding { max-width: 80rem; @@ -679,9 +818,9 @@ h1, h2, h3, h4, h5, h6 { } } -/* Shadow Effects - Updated for new primary color */ +/* Schatten und Glow leiten sich aus dem Akzent der jeweiligen Welt ab. */ .shadow-elegant { - box-shadow: 0 10px 30px -10px rgba(0, 136, 204, 0.2); + box-shadow: 0 10px 30px -10px hsl(var(--secondary) / 0.2); } .shadow-elegant-white { @@ -695,34 +834,34 @@ h1, h2, h3, h4, h5, h6 { .elegant-shadow-card { box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.08), - 0 4px 10px 0px rgba(0, 136, 204,1); + 0 4px 10px 0px hsl(var(--secondary)); } .shadow-premium { - box-shadow: 0 20px 60px -15px rgba(0, 136, 204, 0.3); + box-shadow: 0 20px 60px -15px hsl(var(--secondary) / 0.3); } /* Glow Effects - Inspired by Möbius band's luminous edge */ .glow-soft { box-shadow: - 0 0 10px rgba(0, 155, 221, 0.15), - 0 0 20px rgba(0, 155, 221, 0.1), - 0 4px 10px -4px rgba(0, 136, 204, 0.2); + 0 0 10px hsl(var(--secondary) / 0.15), + 0 0 20px hsl(var(--secondary) / 0.1), + 0 4px 10px -4px hsl(var(--secondary) / 0.2); } .glow-medium { box-shadow: - 0 0 15px rgba(0, 155, 221, 0.25), - 0 0 30px rgba(0, 155, 221, 0.15), - 0 5px 10px -5px rgba(0, 136, 204, 0.3); + 0 0 15px hsl(var(--secondary) / 0.25), + 0 0 30px hsl(var(--secondary) / 0.15), + 0 5px 10px -5px hsl(var(--secondary) / 0.3); } .glow-strong { box-shadow: - 0 0 30px rgba(0, 155, 221, 0.35), - 0 0 60px rgba(0, 155, 221, 0.2), - 0 0 90px rgba(0, 113, 168, 0.1), - 0 10px 10px -10px rgba(0, 136, 204, 0.4); + 0 0 30px hsl(var(--secondary) / 0.35), + 0 0 60px hsl(var(--secondary) / 0.2), + 0 0 90px hsl(var(--secondary-dark) / 0.1), + 0 10px 10px -10px hsl(var(--secondary) / 0.4); } .icon-secondary-linear { background-color: hsl(var(--secondary) / 0.1); @@ -748,6 +887,10 @@ h1, h2, h3, h4, h5, h6 { ); position: relative; } + & section.cta-band { + background: var(--cta-band-gradient); + position: relative; + } & section.bg-accent { background: linear-gradient( 135deg, @@ -785,9 +928,9 @@ h1, h2, h3, h4, h5, h6 { background: linear-gradient( 145deg, - hsl(0 0% 100%) 0%, - hsl(0 0% 99.5%) 40%, - hsl(0 0% 99%) 100% + hsl(var(--card)) 0%, + hsl(var(--card)) 40%, + hsl(var(--card) / 0.92) 100% ); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), @@ -853,7 +996,7 @@ h1, h2, h3, h4, h5, h6 { 0 4px 8px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.1), 0 16px 32px rgba(0, 0, 0, 0.08), - 0 0 40px hsla(199, 74%, 49%, 0.3); + 0 0 40px hsl(var(--secondary) / 0.3); transform: translateY(-2px); filter: brightness(1.08); } @@ -874,3 +1017,67 @@ h1, h2, h3, h4, h5, h6 { transition: none; } } + +/* Herkunfts-Sektion: schematische Standortkarte (Bielefeld als Drehscheibe) */ +.origin-map { + width: 100%; + height: auto; + font-family: inherit; + overflow: visible; +} + +.origin-map-region { + fill: hsl(var(--secondary) / 0.06); + stroke: hsl(var(--secondary) / 0.45); + stroke-width: 1.5; + stroke-dasharray: 4 6; +} + +.origin-map-region-label { + fill: hsl(var(--secondary-deep, var(--secondary))); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.origin-map-spoke { + stroke: hsl(var(--secondary)); + stroke-width: 2; + stroke-dasharray: 6 6; + opacity: 0.7; +} + +.origin-map-hub { + fill: hsl(var(--secondary)); +} + +.origin-map-hub-ring { + fill: hsl(var(--secondary) / 0.18); +} + +.origin-map-dot { + fill: hsl(var(--primary)); + stroke: hsl(var(--card)); + stroke-width: 3; +} + +.origin-map-label { + fill: hsl(var(--foreground)); + font-size: 15px; + font-weight: 500; +} + +.origin-map-sublabel { + fill: hsl(var(--muted-foreground)); + font-size: 12px; +} + +.origin-map-distance { + fill: hsl(var(--muted-foreground)); + font-size: 11px; + text-anchor: middle; + stroke: hsl(var(--card)); + stroke-width: 5px; + paint-order: stroke; +} diff --git a/resources/css/web/theme-b2in.css b/resources/css/web/theme-b2in.css index 7cd27735..be4ea3f2 100644 --- a/resources/css/web/theme-b2in.css +++ b/resources/css/web/theme-b2in.css @@ -1,76 +1,470 @@ @import "tailwindcss"; -/* B2IN Domain Theme - Anthracite & Dynamic Blue */ +/* B2in – zwei Farbwelten auf einem Themensystem (Arbeitsplan September 2026, Punkt 1.1). + + Standard ist die Interior-Welt: Tanne, Creme, Sand, Ocker, Clay, Anthrazit. + Die Immobilien-Welt behält das bestehende Blau; sie greift, sobald das Layout + `world-immobilien` an den Body setzt (siehe b2in_world() in app/helpers.php). + Welche Logo-Variante dazu erscheint, entscheidet ThemeHelper::getLogoPath(). + + Alle Werte sind HSL-Tripel ohne Funktionsaufruf, damit `hsl(var(--x) / 0.5)` + in shared-styles.css und in den Tailwind-Utilities funktioniert. */ @layer base { :root { - /* Base colors */ - --background: 32 20% 97%; /* #f5f4f2 - Light Beige */ - --foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */ + /* ── Interior-Welt ─────────────────────────────────────────────── */ - /* Card colors */ - --card: 0 25% 96%; /* #hsl(0 25% 96%) - Off White */ - --card-foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */ + /* Grundflächen */ + --background: 38 33% 95%; /* #f7f4ef Creme – Hintergrund */ + --foreground: 26 8% 16%; /* #2d2926 Anthrazit – Text */ - --popover: 0 25% 96%; /* #hsl(0 25% 96%)- Off White */ - --popover-foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */ + /* Karten eine Stufe heller als Creme, damit sie sich vom Grund abheben */ + --card: 40 43% 98%; /* #fbf9f5 */ + --card-foreground: 26 8% 16%; + --popover: 40 43% 98%; + --popover-foreground: 26 8% 16%; - /* B2IN Theme Colors */ - --primary: 209 32% 25%; /* #2b3f51 - Anthracite */ - --primary-light: 209 32% 35%; /* #2b3f51 - Anthracite */ - --primary-lighter: 209 32% 85%; /* #2b3f51 - Anthracite */ - --primary-dark: 209 32% 20%; /* #2b3f51 - Anthracite */ - --primary-foreground: 0 25% 96%; /* #hsl(0 25% 96%) - Off White */ + /* Hauptton dunkel: Wortmarke, Buttons, dunkle Sektionen */ + --primary: 175 16% 15%; /* #202c2b Tanne */ + --primary-light: 175 13% 22%; + --primary-lighter: 175 8% 82%; + --primary-dark: 175 18% 11%; + --primary-foreground: 38 33% 95%; /* Creme auf Tanne */ - --secondary: 199 74% 49%; /* #20a0da - Dynamic Blue */ - --secondary-light: 199 74% 56%; /* #20a0da - Dynamic Blue */ - --secondary-lighter: 199 74% 75%; /* #20a0da - Dynamic Blue */ - --secondary-dark: 199 74% 42%; /* #20a0da - Dynamic Blue */ - --secondary-foreground: 0 25% 96%; /* hsl(0 25% 96%) - Off White */ + /* Akzent: Logo-Bogen, Hervorhebungen, Icons, aktive Navigation */ + --secondary: 37 62% 55%; /* #d39e46 Ocker */ + --secondary-light: 37 66% 63%; + --secondary-lighter: 37 60% 80%; + --secondary-dark: 37 55% 45%; + --secondary-deep: 37 50% 36%; /* Ocker als Textfarbe auf hellem Grund */ + --cta-band-line: 37 62% 55%; /* Haarlinie am Abschlussband in Ocker */ + --secondary-foreground: 38 33% 95%; - /* Neutral colors */ - --muted: 0 0% 92%; /* hsl(0 0% 92%) - Light Muted */ - --muted-foreground: 199 50% 35%; /* #4a8bb5 - Muted Blue */ + /* Sekundärton für Produkte und ruhige Beschriftungen */ + --clay: 24 15% 57%; /* #a38f82 Clay */ - --accent: 210 0% 90%; /* hsl(210 5.26% 92.55%) - Light Accent */ - --accent-dark: 210 0% 85%; /* hsl(210 5.26% 92.55%) - Light Accent */ - --accent-foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */ + /* Ruhige Flächen und Fließtext in gedämpfter Farbe */ + --muted: 30 24% 90%; /* helles Sand */ + --muted-foreground: 24 12% 40%; /* abgedunkelter Clay – lesbar auf Creme */ - /* Dark section colors */ - --dark-bg: 209 32% 25%; /* Matching primary */ - --dark-text: 0 0% 100%; /* hsl(0 0% 100%) - Off White */ - --dark-muted: 0 0% 80%; /* hsl(0 0% 80%) - Dark Muted */ + /* Sektionswechsel: Sand als Fläche */ + --accent: 28 25% 80%; /* #d9cbbf Sand */ + --accent-dark: 28 25% 74%; + --accent-foreground: 26 8% 16%; - /* Interactive elements */ - --destructive: 0 84.2% 60.2%; /* #ef4444 - Red */ - --destructive-foreground: 210 40% 98%; /* #f8fafc - Light */ + /* Dunkle Sektionen */ + --dark-bg: 175 16% 15%; /* Tanne */ + --dark-text: 38 33% 95%; + --dark-muted: 38 20% 76%; - --border: 0 0% 80%; /* hsl(0 0% 80%) - Light Border */ - --input: 0 0% 92%; /* hsl(0 0% 92%) - Input Background */ - --ring: 20 14% 16%; /* #2a2a2a - Focus Ring */ + /* Footer: Sand mit Tanne (laut Farbwelt „Sand – Flächen, Footer“) */ + --footer-bg: 28 25% 80%; + --footer-fg: 175 16% 15%; + --footer-muted: 24 12% 40%; + --footer-line: 175 16% 15%; + + /* Abschlussband: Ton in Ton von dunklem Tannengrün in ein helleres, kein Ocker im + Verlauf – Grün zu Ocker wirkte unharmonisch. Ocker bleibt Akzent für Wörter und Linien. */ + --cta-band-end: 175 12% 26%; + + /* Interaktion */ + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 28 20% 84%; + --input: 40 40% 98%; + --ring: 175 16% 15%; --radius: 0.75rem; - /* Hero container background */ - --hero-container: 0 0% 93%; /* #e8e8e8 - Light Gray */ - --hero-container-dark: 0 0% 85%; /* #e8e8e8 - Light Gray */ - --hero-container-light: 0 0% 95%; /* #e8e8e8 - Light Gray */ + /* Bühne des Hero-Bereichs */ + --hero-container: 30 28% 89%; + --hero-container-dark: 28 25% 83%; + --hero-container-light: 36 32% 93%; - /* Consistent shadows */ + /* Schatten */ --shadow-warm: 0 10px 30px -15px hsl(var(--foreground) / 0.1); --shadow-card: 0 4px 20px -8px hsl(var(--foreground) / 0.08); --shadow-elevated: 0 20px 40px -20px hsl(var(--foreground) / 0.15); --shadow-accent-glow: 0 0 30px hsl(var(--secondary) / 0.3); - /* Transitions */ + /* Übergänge */ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); - /* Font families */ + /* Schriften: Inter für Fließtext, eine Serife für Überschriften – der Ton der + Shop-Entwürfe. EB Garamond liegt bereits lokal vor (public/fonts/eb-garamond). */ --font-primary: 'Inter', system-ui, -apple-system, sans-serif; + --font-secondary: 'EB Garamond', 'Georgia', 'Times New Roman', serif; + } + + /* ── Immobilien-Welt: das bisherige Blau ───────────────────────── */ + .world-immobilien { + --background: 32 20% 97%; /* #f5f4f2 helles Beige */ + --foreground: 20 14% 16%; /* #2a2a2a */ + + --card: 0 25% 96%; + --card-foreground: 20 14% 16%; + --popover: 0 25% 96%; + --popover-foreground: 20 14% 16%; + + --primary: 209 32% 25%; /* #2b3f51 Anthracite */ + --primary-light: 209 32% 35%; + --primary-lighter: 209 32% 85%; + --primary-dark: 209 32% 20%; + --primary-foreground: 0 25% 96%; + + --secondary: 199 74% 49%; /* #20a0da Dynamic Blue */ + --secondary-light: 199 74% 56%; + --secondary-lighter: 199 74% 75%; + --secondary-dark: 199 74% 42%; + --secondary-deep: 199 74% 36%; + --secondary-foreground: 0 25% 96%; + + --clay: 199 50% 35%; + + --muted: 0 0% 92%; + --muted-foreground: 199 50% 35%; /* #4a8bb5 */ + + --accent: 210 0% 90%; + --accent-dark: 210 0% 85%; + --accent-foreground: 20 14% 16%; + + --dark-bg: 209 32% 25%; + --dark-text: 0 0% 100%; + --dark-muted: 0 0% 80%; + + --footer-bg: 209 32% 25%; + --footer-fg: 0 0% 100%; + --footer-muted: 0 0% 80%; + --footer-line: 0 0% 80%; + + --cta-band-end: 199 74% 42%; + + --border: 0 0% 80%; + --input: 0 0% 92%; + --ring: 20 14% 16%; + + --hero-container: 0 0% 93%; + --hero-container-dark: 0 0% 85%; + --hero-container-light: 0 0% 95%; + --font-secondary: 'IBM Plex Sans', 'Inter', system-ui, -apple-system, sans-serif; } + + /* ── Interior, Variante „Modern“ (Nachtrag 18) ────────────────────── + Marcels Rückmeldung vom 23.09.: die Creme-Flächen wirken altbacken. Diese + Variante lässt Tanne und Ocker aus dem Logo unangetastet und hellt nur die + Flächen auf: Grund fast weiß, Karten weiß, Sektionswechsel in hellem Grau- + Beige, Footer in Tanne. Aktiv, sobald der Farb-Schalter (palette-switch) + `palette-modern` an setzt; die Wahl liegt im localStorage. */ + .palette-modern .world-interior { + --background: 40 25% 98%; /* #fbfaf8 fast weiß, leicht warm */ + --card: 0 0% 100%; /* Karten reinweiß */ + --popover: 0 0% 100%; + + --muted: 40 12% 94%; /* #f2f0ed */ + --muted-foreground: 24 8% 38%; + + --accent: 36 14% 93%; /* #efece7 Sektionswechsel, klar statt Sand */ + --accent-dark: 36 14% 88%; + + --border: 36 12% 88%; /* #e6e2dc */ + --input: 0 0% 100%; + + --hero-container: 40 14% 95%; + --hero-container-dark: 40 12% 90%; + --hero-container-light: 40 25% 98%; + + /* Footer dunkel in Tanne – gibt der hellen Seite unten einen klaren Abschluss */ + --footer-bg: 175 16% 15%; + --footer-fg: 38 33% 95%; + --footer-muted: 38 20% 76%; + --footer-line: 38 20% 76%; + + /* Karten sind jetzt weiß auf fast weiß: etwas mehr Schatten, damit sie sich lösen */ + --shadow-card: 0 6px 24px -10px hsl(var(--foreground) / 0.14); + --shadow-elevated: 0 24px 48px -20px hsl(var(--foreground) / 0.2); + } + + /* ── Interior, Entwurf „Greige“ (Nachtrag 19, Briefing Marcel 24.09.) ───── + Heller, leicht kühler Grundton (Greige statt Creme), deutlich präsenteres + Tannengrün, Gold nur als feine Linie. Liegt als Klasse am der + Entwurfsseite /entwurf – nicht am wie der Browser-Schalter, damit + sich beide nicht überlagern. */ + .world-interior.palette-greige { + --background: 45 14% 93%; /* #f0eeeb Greige, Off-White */ + --foreground: 166 25% 13%; /* Tannengrau für Text */ + + --card: 40 20% 97%; /* #f8f7f4 – hell, aber kein steriles Weiß */ + --card-foreground: 166 25% 13%; + --popover: 40 20% 97%; + --popover-foreground: 166 25% 13%; + + --primary: 165 44% 14%; /* #14342c Tannengrün, klar und dunkel */ + --primary-light: 165 32% 22%; + --primary-lighter: 165 12% 82%; + --primary-dark: 165 50% 10%; + --primary-foreground: 45 20% 95%; + + --secondary: 38 45% 46%; /* #aa8341 gedecktes Gold */ + --secondary-light: 38 45% 56%; + --secondary-lighter: 40 40% 80%; + --secondary-dark: 38 48% 38%; + --secondary-deep: 38 55% 34%; /* Gold als Textfarbe auf hellem Grund */ + --cta-band-line: 38 45% 46%; + --secondary-foreground: 45 20% 95%; + + --clay: 30 8% 50%; + + --muted: 45 12% 90%; + --muted-foreground: 160 8% 36%; + + --accent: 45 12% 89%; /* #e8e6e0 Sektionswechsel */ + --accent-dark: 45 12% 84%; + --accent-foreground: 166 25% 13%; + + --dark-bg: 165 44% 14%; + --dark-text: 45 20% 95%; + --dark-muted: 45 15% 78%; + + /* Footer in Tannengrün, Haarlinie in Gold */ + --footer-bg: 165 44% 14%; + --footer-fg: 45 20% 95%; + --footer-muted: 45 15% 78%; + --footer-line: 38 45% 46%; + + --cta-band-end: 165 30% 22%; + + --border: 40 10% 84%; + --input: 40 20% 98%; + --ring: 165 44% 14%; + + --hero-container: 40 20% 97%; + --hero-container-dark: 45 12% 90%; + --hero-container-light: 40 25% 98%; + + --shadow-card: 0 6px 24px -10px hsl(var(--foreground) / 0.14); + --shadow-elevated: 0 30px 60px -28px hsl(var(--primary) / 0.35); + } } +/* Interior: Ocker ist Akzent, kein Flächenton unter Text – Creme auf Ocker erreicht + keinen ausreichenden Kontrast. Der zweite Hero-Button wird deshalb zur Kontur in + Tanne, und Ocker-Text auf Sand (Footer) nutzt die tiefe Stufe. */ +.world-interior .btn-secondary-accent { + background: transparent !important; + color: hsl(var(--primary)); + border: 1px solid hsl(var(--primary) / 0.55); + box-shadow: none; +} + +.world-interior .btn-secondary-accent:hover { + background: hsl(var(--primary)) !important; + color: hsl(var(--primary-foreground)); + border-color: hsl(var(--primary)); +} + +.world-interior .section-footer .text-secondary { + color: hsl(var(--secondary-deep)); +} + +/* Variante „Modern“: auf dem dunklen Footer trägt Ocker in voller Stärke. */ +.palette-modern .world-interior .section-footer .text-secondary { + color: hsl(var(--secondary)); +} + +/* Variante „Modern“: Abschlussband und Footer sind beide dunkel – eine helle Haarlinie trennt sie. */ +.palette-modern .world-interior .section-footer { + border-top: 1px solid hsl(var(--footer-muted) / 0.35); +} + +/* Der schwebende Termin-Button liegt über Bildern und weißen Karten: er bekommt eine + Fläche in Kartenfarbe, damit die Kontur nicht im Hintergrund verschwindet. */ +.world-interior .appointment-toggle.btn-secondary-accent { + background: hsl(var(--card)) !important; +} + +.world-interior .appointment-toggle.btn-secondary-accent:hover { + background: hsl(var(--primary)) !important; +} + +/* Cookie-Manager (acme/cookie-consent) in der Interior-Welt: die Paketfarben aus + config/cookie-consent.php sind das Blau der Immobilien-Welt. Hier folgt der schwebende + Button dem Akzent-Button der Seite (Tanne, Hover heller), Icon, Schalter und Link-Hover + tragen Ocker, „Speichern“ liegt in Tanne. Die Immobilien-Welt behält ihr Blau. */ +.world-interior .cookie-consent-wrapper { + --cc-primary: hsl(var(--secondary)); + --cc-primary-hover: hsl(var(--secondary-dark)); + --cc-save: hsl(var(--primary)); + --cc-save-hover: hsl(var(--primary-light)); + --cc-button-bg: hsl(var(--primary)); + --cc-button-hover: hsl(var(--primary-light)); +} + +.world-interior .cookie-consent-wrapper .cc-floating-btn { + --tw-ring-color: hsl(var(--secondary)) !important; + color: hsl(var(--primary-foreground)); + background: linear-gradient(145deg, hsl(var(--primary)) 0%, hsl(var(--primary-light)) 100%); +} + +.world-interior .cookie-consent-wrapper .cc-floating-btn:hover { + background: linear-gradient(145deg, hsl(var(--primary-light)) 0%, hsl(var(--primary)) 100%); +} + +/* Variante „Modern“: der Footer ist dunkel, also das negative Logo statt des positiven. */ +.footer-logo-dark { + display: none; +} + +.palette-modern .world-interior .footer-logo-light { + display: none; +} + +.palette-modern .world-interior .footer-logo-dark { + display: block; +} @import "./shared-styles.css"; + +/* ── Entwurf „Greige“: Footer dunkel mit Goldlinie und negativem Logo ─────── */ +.world-interior.palette-greige .section-footer { + border-top: 1px solid hsl(var(--secondary) / 0.6); +} + +.world-interior.palette-greige .section-footer .text-secondary { + color: hsl(var(--secondary)); +} + +.world-interior.palette-greige .footer-logo-light { + display: none; +} + +.world-interior.palette-greige .footer-logo-dark { + display: block; +} + +/* ── Hero des Entwurfs: Rahmen mit Goldkante, grüne Flanken, Slider ──────── + Goldkanten als Verlaufs-Border: transparenter Rand, Fläche auf padding-box, + Goldverlauf auf border-box. Kein Pseudo-Rand mit Maske mehr – der ließ an + der gerundeten Kante eine helle Linie durchscheinen. Die grünen Flanken sind + Inline-SVGs (components/web/hero-flank-*), skaliert über die Rahmenhöhe. */ +.hero-draft .hero-frame { + border: 1.5px solid transparent; + background: + linear-gradient(hsl(var(--hero-container)), hsl(var(--hero-container))) padding-box, + linear-gradient( + 125deg, + hsl(var(--secondary-light)) 0%, + hsl(var(--secondary)) 28%, + hsl(var(--secondary) / 0.35) 62%, + hsl(var(--secondary-light)) 100% + ) border-box; + box-shadow: var(--shadow-elevated); + isolation: isolate; +} + +.hero-draft .hero-flank { + position: absolute; + z-index: 0; + width: auto; +} + +.hero-draft .hero-flank-right { + top: 0; + right: 0; + height: 100%; +} + +.hero-draft .hero-flank-left { + left: 0; + bottom: 0; + height: 46%; +} + +.hero-draft .hero-image { + position: relative; + z-index: 1; +} + +/* Goldkante um das Bild, gleiche Technik wie am Rahmen */ +.hero-draft .hero-image-frame { + border: 1.5px solid transparent; + border-radius: 1.25rem; + background: + linear-gradient(hsl(var(--card)), hsl(var(--card))) padding-box, + linear-gradient( + 135deg, + hsl(var(--secondary-light)) 0%, + hsl(var(--secondary)) 40%, + hsl(var(--secondary) / 0.4) 100% + ) border-box; + box-shadow: var(--shadow-elevated); +} + +.hero-draft .hero-eyebrow { + display: inline-flex; + align-items: center; + gap: 1rem; + font-size: 0.75rem; + letter-spacing: 0.28em; + text-transform: uppercase; + color: hsl(var(--muted-foreground)); +} + +.hero-draft .hero-eyebrow::after { + content: ""; + width: 3.5rem; + height: 1px; + background: hsl(var(--secondary)); +} + +.hero-draft .hero-title { + font-family: var(--font-secondary); + font-size: clamp(2rem, 3.1vw, 3.3rem); + line-height: 1.12; + font-weight: 400; + letter-spacing: -0.01em; + color: hsl(var(--primary)); +} + +.hero-draft .hero-title em { + font-style: normal; + color: hsl(var(--secondary-deep)); +} + +/* Button: Goldrand rundum, unten kräftiger, Pfeil in Gold */ +.hero-draft .btn-hero { + display: inline-flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.75rem; + border-radius: 0.75rem; + font-weight: 500; + color: hsl(var(--primary-foreground)); + background: linear-gradient(145deg, hsl(var(--primary)) 0%, hsl(var(--primary-light)) 100%); + border: 1px solid hsl(var(--secondary)); + border-bottom-width: 3px; + box-shadow: 0 14px 30px -14px hsl(var(--primary) / 0.6); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.hero-draft .btn-hero svg { + color: hsl(var(--secondary-light)); +} + +.hero-draft .btn-hero:hover { + transform: translateY(-1px); + box-shadow: 0 18px 34px -14px hsl(var(--primary) / 0.7); +} + +.hero-draft .hero-slide { + transition: opacity 1.4s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .hero-draft .hero-slide { + transition: none; + } +} + diff --git a/resources/js/app.js b/resources/js/app.js index 88d489ef..bfb3b50a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -75,6 +75,22 @@ globalObserver.observe(el); }); + // Terminanfrage: Links auf die Kontaktseite mit data-open-appointment oeffnen das + // schwebende Widget statt die Seite zu wechseln. Das Anliegen kommt aus + // data-appointment-topic (z. B. der Marken-Slug). Nur einmal registrieren. + if (!window.appointmentLinksInitialized) { + document.addEventListener('click', function(e) { + const trigger = e.target.closest('a[data-open-appointment]'); + if (!trigger) return; + + e.preventDefault(); + window.dispatchEvent(new CustomEvent('open-appointment', { + detail: { topic: trigger.dataset.appointmentTopic || null } + })); + }); + window.appointmentLinksInitialized = true; + } + // Smooth Scroll für Anchor-Links (nur einmal registrieren) if (!window.smoothScrollInitialized) { document.addEventListener('click', function(e) { diff --git a/resources/lang/de/b2in.php b/resources/lang/de/b2in.php index 44329f1a..35d44036 100644 --- a/resources/lang/de/b2in.php +++ b/resources/lang/de/b2in.php @@ -5,45 +5,111 @@ return [ 'b2in' => [ 'announcement_bar' => [ 'enabled' => true, - 'id' => 'azizi-launch-2026', - 'badge' => 'NEW LAUNCH', - 'text' => 'Azizi Creek Views 4 – Exklusives Off-Market-Projekt in Dubai '.\App\Helpers\PriceHelper::formatAed(1_125_000, 'ab'), + 'id' => 'interior-rotpunkt-2026', + 'badge' => 'NEU', + 'text' => 'Rotpunkt Küchen jetzt bei B2in Interior – jetzt Beratungstermin sichern', + 'link_text' => 'Termin anfragen', + 'link_url' => '/contact', + ], + // Ankündigungsleiste der Immobilien-Welt – eigenes Thema, eigener Dismiss-Schlüssel. + 'announcement_bar_immobilien' => [ + 'enabled' => true, + 'id' => 'azizi-creek-views-4-2026', + 'badge' => 'NEU', + 'text' => 'Azizi Creek Views 4 – Off-Market-Projekt in Dubai ab 1.125.000 AED', 'link_text' => 'Exposé ansehen', 'link_url' => '/immobilien/azizi-creek-views-4', ], + 'appointment_widget' => [ + 'button_label' => 'Termin anfragen', + 'title' => 'Termin anfragen', + 'intro' => 'Sagen Sie uns kurz, worum es geht – wir melden uns und stimmen einen Termin ab. Persönlich, unverbindlich.', + 'success' => 'Vielen Dank! Wir melden uns zeitnah bei Ihnen und stimmen den Termin ab.', + 'whatsapp_text' => 'Hallo B2in Interior, ich hätte gern einen Beratungstermin.', + ], + 'referenzen_hero' => [ + 'title' => 'Unsere Referenzen', + 'text' => 'Hier zeigen wir ausschließlich Arbeiten, die wir tatsächlich ausgeführt haben. Der Bereich wächst mit jedem Auftrag, den wir zeigen dürfen.', + 'image' => '', + 'image_alt' => 'B2in Interior – realisierte Projekte', + ], + 'reference_projects' => [ + 'title' => 'Realisierte Projekte', + 'text' => 'Ausgeführte Arbeiten aus unseren Aufträgen, sortiert nach Marke. Entwürfe und Visualisierungen zeigen wir bewusst getrennt davon – auf den Markenseiten.', + 'empty' => 'Die ersten Projekte sind in Arbeit und erscheinen hier, sobald wir sie zeigen dürfen. Bis dahin schauen wir uns gern gemeinsam an, was für Ihre Räume möglich ist.', + ], + 'display_feed' => [ + 'title' => 'Aktuelles aus unserem Showroom', + 'text' => 'Was gerade auf dem Bildschirm im Showroom läuft, sehen Sie hier live – dieselbe Ansicht, derselbe Stand.', + 'note' => 'Der Inhalt wechselt automatisch.', + 'link_text' => 'Zu den Referenzen', + 'link_url' => '/referenzen', + 'iframe_title' => 'Live-Ansicht des Showroom-Displays', + 'display_id' => '', + ], + 'footer' => [ + 'claim' => 'Premium-Einrichtung. Persönlich gemacht.', + 'byline' => 'Marcel Scheibe', + 'links' => [ + ['label' => 'Häufige Fragen', 'url' => '/faq'], + ['label' => 'Datenschutz', 'url' => '/privacy'], + ['label' => 'AGB', 'url' => '/terms'], + ['label' => 'Cookie-Richtlinie', 'url' => '/cookie-policy'], + ['label' => 'Impressum', 'url' => '/impressum'], + ], + ], 'header' => [ 'portal_login' => 'Partner-Login', 'navigation' => [ ['label' => 'Home', 'url' => '/'], - ['label' => 'Immobilien', 'url' => '/immobilien'], - ['label' => 'Netzwerk', 'url' => '/netzwerk'], + ['label' => 'Marken', 'url' => '/marken'], + ['label' => 'Referenzen', 'url' => '/referenzen'], ['label' => 'Magazin', 'url' => '/magazin'], - ['label' => 'FAQ', 'url' => '/faq'], ['label' => 'Über B2in', 'url' => '/about'], ], ], + // Navigation der Immobilien-Welt. „Immobilien“ steht nicht in der Interior-Navigation + // und umgekehrt – der Welten-Wechsler in Kopf- und Fußzeile ist die einzige Brücke. + 'header_immobilien' => [ + 'portal_login' => 'Partner-Login', + 'navigation' => [ + ['label' => 'Immobilien', 'url' => '/immobilien'], + ['label' => 'Projekte', 'url' => '/immobilien#projekte'], + ['label' => 'Magazin', 'url' => '/immobilien/magazin'], + ['label' => 'Über B2in', 'url' => '/immobilien/ueber-b2in'], + ], + ], 'hero' => [ - 'title' => 'B2in – Wo exklusive Immobilien-Investments auf smartes Interior treffen.', - 'subtitle' => 'Ihr Partner für renditestarke internationale Immobilien und globales Supply-Chain-Management im Interior-Bereich.', + 'title' => 'B2in Interior – Premium-Einrichtung von starken Marken. Für Ihr Zuhause. Für Ihr Projekt.', + 'subtitle' => 'Von der einzelnen Maßanfertigung bis zur vollständigen Ausstattung – mit CABINET, Rotpunkt Küchen, Febrü und einem Netzwerk regionaler Fachexperten.', 'image' => 'b2in/hero-room.jpg', - 'image_alt' => 'B2in – Internationale Immobilien und exklusive Einrichtungskonzepte', - 'cta1_text' => 'Zu den Immobilien-Projekten', - 'cta1_link' => '/immobilien', - 'cta2_text' => 'Unser Netzwerk', - 'cta2_link' => '/netzwerk', + 'image_alt' => 'B2in Interior – Premium-Einrichtung von starken Marken', + 'cta1_text' => 'Unsere Marken entdecken', + 'cta1_link' => '/marken', + 'cta2_text' => 'Termin anfragen', + 'cta2_link' => '/contact', 'stats' => [ - 'Internationale Immobilien', - 'Exklusive Einrichtung', + 'Premium-Marken', + 'Maßgefertigte Lösungen', 'Persönliche Beratung', ], - 'card_title' => 'B2in', - 'card_text' => 'Connecting Design and Property', + 'card_title' => 'B2in Interior', + 'card_text' => 'Einrichtung, die bleibt.', + ], + 'hero_slider' => [ + 'eyebrow' => 'B2in Interior', + 'title' => 'Räume aus einer Hand. Mit starken Marken und persönlichen Lösungen.', + 'subtitle' => 'Wir vereinen CABINET, Rotpunkt Küchen, Febrü und ein Netzwerk regionaler Experten, um individuelle Interior-Lösungen für Ihr Zuhause oder Ihr Projekt zu realisieren.', + 'cta_text' => 'Unsere Marken entdecken', + 'cta_link' => '/marken', + 'interval' => 7, + 'slides' => [], ], 'founder_bar' => [ 'image' => 'b2in/marcel-scheibe.jpg', 'name' => 'Marcel Scheibe', 'title' => 'Gründer & CEO, B2in', - 'statement' => 'B2in by Marcel Scheibe – Ihr persönlicher Partner für internationale Immobilien und exklusive Einrichtungskonzepte.', + 'statement' => 'B2in by Marcel Scheibe – Ihr persönlicher Partner für hochwertige Einrichtung und starke Marken.', ], 'synergie_section' => [ 'title' => 'Zwei Welten. Ein Netzwerk.', @@ -81,8 +147,8 @@ return [ 'vision_section' => [ 'title' => 'Gebaut auf Expertise und Vertrauen', 'paragraphs' => [ - 'B2in (Bridges2international) verbindet zwei Welten: internationale Immobilien und exklusive Einrichtungskonzepte. Als Ihr persönlicher Partner navigiere ich Sie durch beide Bereiche – mit Expertise, Netzwerk und dem Anspruch, dass jede Entscheidung auf Vertrauen basiert.', - 'Ob ein Investment in Dubai, eine Villa in Lissabon oder die maßgeschneiderte Einrichtung Ihres neuen Zuhauses durch lokale Fachexperten – bei B2in laufen alle Fäden zusammen.', + 'B2in stand ursprünglich für Bridges to International. Heute steht es ebenso für Bridges to Interior: hochwertige Einrichtung mit persönlicher Handschrift. Als Ihr Ansprechpartner verbinde ich ausgewählte Hersteller, regionale Fachexperten und ein verlässliches Netzwerk – damit aus Räumen Orte werden, die bleiben.', + 'Ob maßgefertigter Einbauschrank, neue Küche oder die vollständige Ausstattung eines Objekts: Bei B2in laufen alle Fäden zusammen. Und wer über die Einrichtung hinausdenkt, findet bei uns auch den Zugang zu internationalen Immobilien.', 'Regional verwurzelt, international vernetzt – das ist B2in.', ], 'image' => 'b2in/marcel-scheibe-about.jpg', @@ -132,12 +198,217 @@ return [ 'image_alt' => 'Das Ergebnis für den Kunden – Immobilie und Einrichtung aus einer Hand', 'image_caption' => 'Immobilie und Einrichtung – aus einer Hand', ], + 'audience_switch' => [ + 'title' => 'Für Ihr Zuhause. Für Ihr Projekt.', + 'subtitle' => 'Ob einzelnes Möbelstück oder komplette Ausstattung – wir begleiten Sie mit denselben Marken, demselben Anspruch und einem persönlichen Ansprechpartner.', + 'cards' => [ + [ + 'icon' => 'home', + 'title' => 'Einrichtung für Ihr Zuhause', + 'description' => 'Einzelne Möbelstücke, maßgefertigte Einbaulösungen oder die komplette Einrichtung: Unsere Kernmarken und unser Fachhandels-Netzwerk begleiten Sie von der ersten Idee bis zur Montage. Besuchen Sie unsere Showrooms oder vereinbaren Sie eine persönliche Beratung.', + 'button_text' => 'Beratung anfragen', + 'button_link' => '/contact', + ], + [ + 'icon' => 'building-office-2', + 'title' => 'Ausstattung für Ihr Projekt', + 'description' => 'Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.', + 'button_text' => 'Projekt besprechen', + 'button_link' => '/contact', + ], + ], + ], + 'core_brands' => [ + 'title' => 'Unsere Kernmarken', + 'subtitle' => 'Wir arbeiten nicht mit beliebigen Katalogen, sondern mit ausgewählten Herstellern, hinter denen wir stehen – jede Marke ein Spezialist auf ihrem Gebiet.', + 'brands' => [ + [ + 'slug' => 'cabinet', + 'name' => 'CABINET', + 'tagline' => 'Einbauschränke nach Maß.', + 'teaser' => 'Maßgefertigte Schranklösungen, geplant bis auf den Millimeter – gefertigt für Räume, in denen Standard nicht genügt. Erleben Sie die Möglichkeiten in unserem Showroom in Bielefeld.', + 'image' => '', + 'logo' => '', + 'link_text' => 'Mehr zu CABINET', + 'origin_city' => 'Köln', + 'origin_distance' => 'rund 200 km', + 'origin_role' => 'Gefertigt in der Kölner Manufaktur, beraten und geplant im CABINET Store Bielefeld.', + ], + [ + 'slug' => 'rotpunkt', + 'name' => 'Rotpunkt Küchen', + 'tagline' => 'Küchen, die Haltung zeigen.', + 'teaser' => 'Premium-Küchen aus deutscher Fertigung – individuell geplant, nachhaltig gedacht und gebaut für den Mittelpunkt Ihres Zuhauses. Beratung und Planung in unserem Küchenstudio.', + 'image' => '', + 'logo' => '', + 'link_text' => 'Mehr zu Rotpunkt', + 'origin_city' => 'Bünde', + 'origin_distance' => 'rund 30 km', + 'origin_role' => 'Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio.', + ], + [ + 'slug' => 'februe', + 'name' => 'Febrü', + 'tagline' => 'Arbeiten, wo man gerne bleibt.', + 'teaser' => 'Möbel für konzentriertes Arbeiten – vom durchdachten Home-Office bis zur kompletten Büroeinrichtung. Ergonomie und Design für Zuhause und fürs Unternehmen.', + 'image' => '', + 'logo' => '', + 'link_text' => 'Mehr zu Febrü', + 'origin_city' => 'Herford', + 'origin_distance' => 'rund 20 km', + 'origin_role' => 'Büro- und Objektmöbel aus eigener Fertigung in Herford.', + ], + [ + 'slug' => 'tischlerei', + 'name' => 'Otterpohl Möbelservice', + 'tagline' => 'Nicht easy click & buy. Aber dennoch easy to get.', + 'teaser' => 'Individuelle Möbel, Türen, Treppen, Küchen, Wandverkleidungen und Objektbau – bis hin zum kompletten Innenausbau. Tischlerhandwerk aus Rheda-Wiedenbrück.', + 'image' => '', + 'logo' => 'marken/otterpohl.png', + 'link_text' => 'Mehr zu Otterpohl', + 'origin_city' => 'Rheda-Wiedenbrück', + 'origin_distance' => 'rund 35 km', + 'origin_role' => 'Tischlerei und Innenausbau aus Rheda-Wiedenbrück – vom einzelnen Möbel bis zum ganzen Raum, inklusive Montage.', + ], + ], + ], + // Herkunfts-Sektion (Arbeitsplan 14.09., Punkt 1.3). Die Standorte kommen aus den Marken + // (origin_city, origin_distance, origin_role – Nachtrag 11), die Kartenpositionen aus + // OriginSection::MAP_POSITIONS. ⚠️ Ablauf CABINET (steps) nach Kevins Verständnis – von Marcel bestätigen lassen. + 'origin_section' => [ + 'title' => 'Aus der Region gefertigt', + 'subtitle' => 'Vier deutsche Manufakturen, drei davon in Ostwestfalen. Kurze Wege, Menschen, die wir kennen, und Qualität, die man besuchen kann.', + 'map_alt' => 'Schematische Karte: Bielefeld als Ausgangspunkt, Fertigung in Bünde, Herford, Rheda-Wiedenbrück und Köln', + 'region_label' => 'Ostwestfalen', + 'distance_suffix' => 'von Bielefeld', + 'hub' => [ + 'city' => 'Bielefeld', + 'role' => 'B2in Interior · Beratung, Planung, Showrooms', + 'x' => 400, + 'y' => 175, + ], + 'link_text' => 'Zur Marke', + 'steps_title' => 'Gefertigt in Köln, geplant in Bielefeld', + 'steps_text' => 'Am Beispiel CABINET: Die Möbel entstehen in der Kölner Fertigung. Beraten, aufgemessen und geplant wird bei uns im CABINET Store Bielefeld. Aufgebaut wird vor Ort – mit unserer Tischlerei, die auf Wunsch auch das umsetzt, was im Katalog nicht steht.', + 'steps' => [ + [ + 'title' => 'Beratung und Planung in Bielefeld', + 'description' => 'Im CABINET Store nehmen wir Maß, planen bis ins Detail und stimmen jede Front und jede Innenaufteilung mit Ihnen ab.', + ], + [ + 'title' => 'Fertigung in Köln', + 'description' => 'CABINET fertigt Ihren Schrank in der eigenen Manufaktur – nach den Maßen aus der Planung.', + ], + [ + 'title' => 'Aufbau vor Ort, Sonderwünsche inklusive', + 'description' => 'Otterpohl baut auf und ergänzt, was der Raum zusätzlich braucht: Blenden, Anschlüsse, Sonderlösungen.', + ], + ], + ], + // Gemeinsamer Handwerk-Abschluss jeder Markenseite (Feedback 17.09., Nachtrag 15). Erscheint auf + // allen Markenseiten außer bei Otterpohl selbst; der Link führt zur Otterpohl-Seite. + 'brand_craft' => [ + 'title' => 'Planung plus Handwerk', + 'paragraphs' => [ + 'CABINET, Rotpunkt und Febrü decken bereits eine enorme planerische und gestalterische Bandbreite ab. Es geht deshalb ausdrücklich nicht um Sonderanfertigungen um jeden Preis.', + 'Natürlich gibt es trotzdem diesen einen Fall, bei dem selbst ein hervorragendes System an seine Grenze kommt: der Übergang zur Architektur, die besondere Verkleidung, die Wand, die noch fehlt, oder der letzte Millimeter. Dann greift unsere Zusammenarbeit mit Otterpohl aus Rheda-Wiedenbrück – Tischlerhandwerk, Innenausbau und Montage aus einer Hand.', + 'Wir wägen gestalterischen Mehrwert, technische Notwendigkeit und wirtschaftliche Sinnhaftigkeit ehrlich miteinander ab. Am Ende entscheiden aber Ihre Wünsche – nicht unser Drang, Ihnen noch eine Sonderlösung zu verkaufen.', + ], + 'detail' => [ + 'title' => 'Wenn das Handwerk den letzten Millimeter übernimmt', + 'paragraphs' => [ + 'Möglich sind beispielsweise passgenaue Nischenlösungen, besondere Verkleidungen, raumhohe Einbauten, Übergänge zu angrenzenden Möbeln, individuelle Lackierungen und Echtholzfurniere oder Anpassungen an die vorhandene Architektur.', + 'Bei Bedarf endet das Konzept nicht am Möbel. Auch Trockenbau, Tapeten, Fußböden, Wandgestaltung, Beleuchtung und weitere Details können einbezogen werden.', + ], + ], + 'claim' => 'Die Möglichkeiten eines Maßsystems. Die Flexibilität des Handwerks.', + 'link_text' => 'Mehr zu Otterpohl', + 'brand_slug' => 'tischlerei', + ], + 'sponsoring' => [ + 'title' => 'Regional verbunden', + 'subtitle' => 'Wir unterstützen Vereine und Organisationen aus der Region – weil Verwurzelung mehr ist als eine Adresse.', + // ⚠️ Die Kurztexte sind Entwürfe – von Marcel bestätigen lassen, bevor es live geht. + 'logos' => [ + [ + 'name' => 'DSC Arminia Bielefeld', + 'image' => 'sponsoring/arminia-bielefeld.png', + 'text' => 'Traditionsverein aus Bielefeld – der Stadt, in der wir zu Hause sind.', + ], + [ + 'name' => 'TC Bad Waldliesborn-Horn', + 'image' => 'sponsoring/tc-bad-waldliesborn-horn.jpg', + 'text' => 'Tennisclub aus Bad Waldliesborn. Vereinssport, der Menschen vor Ort zusammenbringt.', + ], + [ + 'name' => 'TC Hövelhof', + 'image' => 'sponsoring/tc-hoevelhof.png', + 'text' => 'Tennisclub aus Hövelhof – Breitensport im Kreis Paderborn.', + ], + ], + ], + 'immobilien_teaser' => [ + 'title' => 'Außerdem: Internationale Immobilien', + 'text' => 'Neben der Einrichtung begleiten wir ausgewählte internationale Immobilienprojekte – persönlich kuratiert und mit klarem Blick auf Substanz.', + 'link_text' => 'Zu den Immobilien', + 'link_url' => '/immobilien', + ], + 'marken_hero' => [ + 'title' => 'Unsere Marken', + 'text' => 'Wir arbeiten nicht mit beliebigen Katalogen, sondern mit ausgewählten Herstellern, hinter denen wir stehen. Jede Marke ist Spezialist auf ihrem Gebiet – und jede bringt mit, was gute Einrichtung ausmacht: Qualität, die man sieht, und Beratung, die zuhört.', + // Platzhalter aus dem Altbestand – gegen ein eigenes Motiv tauschen, sobald geliefert. + 'image' => 'b2in/b2a.jpg', + 'image_alt' => 'B2in Interior – unsere Kernmarken', + ], + 'marken_network' => [ + 'title' => 'Mehr als Marken: unser Netzwerk', + 'paragraphs' => [ + 'Gute Einrichtung endet nicht beim Möbelstück. Deshalb arbeiten wir mit Fachbetrieben und Handwerkern aus der Region zusammen – von der Aufmaß-Erstellung über die Montage bis zu angrenzenden Gewerken.', + 'Für Sie bedeutet das: ein Ansprechpartner, der die Fäden zusammenhält, statt vieler Einzelgespräche. Und Partner, die wir kennen und für deren Arbeit wir geradestehen.', + ], + 'points' => [ + [ + 'icon' => 'pencil-square', + 'title' => 'Aufmaß & Planung', + 'description' => 'Wir kommen zu Ihnen, nehmen Maß und planen mit Ihnen gemeinsam.', + ], + [ + 'icon' => 'wrench-screwdriver', + 'title' => 'Montage durch Fachbetriebe', + 'description' => 'Handwerker aus der Region, die wir persönlich kennen.', + ], + [ + 'icon' => 'user', + 'title' => 'Ein Ansprechpartner', + 'description' => 'Von der ersten Idee bis zur Übergabe koordiniert aus einer Hand.', + ], + ], + ], + 'marken_growth' => [ + 'enabled' => true, + 'title' => 'Unsere Markenwelt wächst', + 'text' => 'Wir erweitern unser Portfolio laufend um Hersteller, die zu unserem Anspruch passen. Sie vermissen eine bestimmte Marke oder haben einen konkreten Wunsch? Sprechen Sie uns an – oft finden wir über unser Netzwerk genau das Richtige.', + ], + 'marken_cta' => [ + 'title' => 'Lernen Sie unsere Marken kennen', + 'text' => 'Am besten dort, wo Sie sie anfassen können: in unseren Showrooms. Vereinbaren Sie einen unverbindlichen Beratungstermin – wir nehmen uns Zeit.', + 'button_text' => 'Termin anfragen', + 'button_link' => '/contact', + 'whatsapp_text' => 'Per WhatsApp schreiben', + ], 'cta_section' => [ 'title' => 'Ihr nächster Schritt', - 'subtitle' => 'Ob Immobilien-Investment, Supply-Chain-Partnerschaft oder Einrichtungs-Netzwerk – sprechen Sie direkt mit uns.', - 'button_text' => 'Kontakt aufnehmen', + 'subtitle' => 'Ob einzelnes Möbelstück, komplette Einrichtung oder Projektausstattung – sprechen Sie mit uns. Persönlich, unkompliziert, verbindlich.', + 'button_text' => 'Termin anfragen', 'button_link' => '/contact', ], + // Abschluss der Immobilien-Welt – fuehrt zur Kontaktseite dieser Welt. + 'cta_section_immobilien' => [ + 'title' => 'Ihr nächster Schritt', + 'subtitle' => 'Ob Exposé, Kaufprozess oder Einrichtung nach dem Kauf – sprechen Sie mit uns. Persönlich, unkompliziert, verbindlich.', + 'button_text' => 'Gespräch anfragen', + 'button_link' => '/immobilien/kontakt', + ], 'immobilien_hero' => [ 'title' => 'Investieren Sie in die Zukunft – Dubai, Lissabon & mehr.', 'subtitle' => 'Exklusive Off-Market-Projekte und High-Yield-Investments. Persönlich kuratiert und begleitet von Marcel Scheibe.', @@ -377,14 +648,14 @@ return [ // NETZWERK SOFT LAUNCH - Kombinierte Teaser-Seite // ============================================================ 'netzwerk_hero' => [ - 'title' => 'Das B2in Ökosystem – Immobilien trifft Interior.', - 'subtitle' => 'Wir bauen aktuell das intelligenteste Local-for-Local Einrichtungsnetzwerk. Als Immobilienkunde von B2in profitieren Sie in Zukunft exklusiv von unserem Closed-Shop.', + 'title' => 'Das B2in Netzwerk – regionale Fachexperten und starke Marken.', + 'subtitle' => 'Wir verbinden Fachhändler aus der Region mit ausgewählten europäischen Herstellern – damit Sie vor Ort beraten werden und trotzdem Zugriff auf das gesamte Sortiment haben.', 'image' => 'b2in/ecosystem-hero.jpg', - 'image_alt' => 'B2in Ökosystem – Einrichtung und Immobilien', + 'image_alt' => 'B2in Netzwerk – Einrichtung und regionale Fachexperten', ], 'netzwerk_image_break' => [ 'image' => 'b2in/ecosystem-hero.jpg', - 'image_alt' => 'B2in Ökosystem – Einrichtung und Immobilien', + 'image_alt' => 'B2in Netzwerk – Einrichtung und regionale Fachexperten', 'quote' => 'Wir verbinden, was zusammengehört: Immobilien und Einrichtung.', 'author' => 'Marcel Scheibe', ], @@ -406,7 +677,7 @@ return [ [ 'icon' => 'globe-alt', 'title' => 'Für Entwickler & Marken', - 'description' => 'Deutsche Vertragssicherheit für internationale Immobilienentwickler. Supply-Chain-Management mit Fokus auf Termintreue, Qualitätskontrolle und Durchsetzungskraft.', + 'description' => 'Deutsche Vertragssicherheit für Objekte und Projektentwicklungen: Beschaffung aus einer Hand, mit Fokus auf Termintreue, Qualitätskontrolle und Durchsetzungskraft.', 'status' => 'Vorab-Registrierung möglich', ], ], @@ -422,7 +693,7 @@ return [ 'title' => 'Unser Premiumpartner: CABINET Einbauschränke', 'lead' => 'Maßgefertigte Exzellenz für höchste Ansprüche.', 'paragraphs' => [ - 'Im B2in-Ökosystem setzen wir auf Qualität, die Bestand hat. Wir sind stolz darauf, CABINET als Premiumpartner in unserem Netzwerk zu präsentieren. Der CABINET Store in Bielefeld (unser physischer Ankerpunkt) steht für maßgefertigte Einbaulösungen, die Design, Funktionalität und handwerkliche Präzision perfekt vereinen.', + 'Im B2in-Netzwerk setzen wir auf Qualität, die Bestand hat. Wir sind stolz darauf, CABINET als Premiumpartner in unserem Netzwerk zu präsentieren. Der CABINET Store in Bielefeld (unser physischer Ankerpunkt) steht für maßgefertigte Einbaulösungen, die Design, Funktionalität und handwerkliche Präzision perfekt vereinen.', 'Ob begehbare Kleiderschränke, smarte Raumlösungen oder exklusive Innenausbauten – CABINET liefert den Premium-Standard, den unsere Immobilien-Investoren und Entwickler für ihre High-End-Projekte fordern. Diese Partnerschaft ist ein lebendiger Beweis für unsere Philosophie: Wir verbinden internationale Immobilien mit deutscher Einrichtungsexzellenz.', ], 'image' => 'b2in/cabinet_logo.png', @@ -571,65 +842,202 @@ return [ ], ], ], - 'faq' => [ - 'title' => 'Häufig gestellte Fragen', - 'subtitle' => 'Hier finden Sie Antworten auf die häufigsten Fragen zu B2in und unserem Ökosystem.', + // ── Immobilien-Welt: Magazin, Über uns und FAQ ────────────────────────────── + // Inhalte aus der Zeit vor dem Interior-Relaunch (Stand Februar 2026), leicht + // angepasst und als Immobilien-Bereich gekennzeichnet. + 'magazin_list_immobilien' => [ + 'title' => 'B2in Immobilien-Magazin', + 'subtitle' => 'Marktanalysen und Praxiswissen rund um Immobilien-Investments in Dubai – Kaufprozess, Stadtteile und die Rolle der Einrichtung für die Rendite.', + 'read_more' => 'Weiterlesen', + 'load_more' => 'Weitere Artikel laden', + ], + 'immobilien_about_hero' => [ + 'title' => 'B2in Immobilien: Unsere Mission', + 'quote' => '"Meine Mission ist es, zwei Welten zu verbinden, die zusammengehören: internationale Immobilien und hochwertige Einrichtung. B2in begleitet Investoren persönlich beim Kauf – und richtet die Immobilie anschließend aus einer Hand ein.

Wir bauen Brücken – zwischen europäischem Design, internationalen Märkten und dem Zuhause der Menschen."', + 'founder_name' => 'Marcel Scheibe', + 'founder_title' => 'Gründer & CEO, B2in', + 'image' => 'b2in/about-hero.jpg', + 'image_alt' => 'Marcel Scheibe, Gründer und CEO von B2in', + 'card_title' => 'B2in Immobilien', + 'card_text' => 'Connecting Design and Property', + ], + 'immobilien_our_story' => [ + 'title' => 'Unsere Geschichte', + 'timeline' => [ + [ + 'title' => 'Die Idee', + 'description' => '2024 erkannten wir eine entscheidende Lücke: Immobilienkäufer suchen nach nahtlosen Einrichtungslösungen, und der Fachhandel vor Ort braucht Sichtbarkeit. Diese beiden Welten gehören zusammen.', + 'icon' => 'light-bulb', + ], + [ + 'title' => 'Das Fundament', + 'description' => 'Wir bauen B2in als Netzwerk auf: Einrichtungsexperten aus der Region, europäische Hersteller und Immobilienprofis – fair, transparent und mit einem persönlichen Ansprechpartner.', + 'icon' => 'rocket-launch', + ], + [ + 'title' => 'Die Erweiterung', + 'description' => '2025/2026 kommen internationale Immobilien dazu: ausgewählte Projekte in Dubai, aus eigener Investorensicht geprüft und persönlich begleitet. Die Einrichtung bleibt der Kern – beide Bereiche ergänzen sich.', + 'icon' => 'globe-alt', + ], + [ + 'title' => 'Die Vision', + 'description' => 'B2in verbindet Design und Immobilien – regional verwurzelt, international vernetzt. In jeder Region der verlässliche Partner für Immobilien-Investments und exklusive Einrichtungskonzepte.', + 'icon' => 'star', + ], + ], + 'summary' => 'Was als Vision begann, den regionalen Handel zu stärken, ist heute ein Netzwerk, das Immobilien und Einrichtung verbindet. B2in schließt die Lücke zwischen internationalen Investments und lokaler Expertise – mit Marcel Scheibe als Gesicht und Ansprechpartner.', + ], + 'immobilien_about_image_break' => [ + 'image' => 'b2in/best-of-two-worlds.jpg', + 'image_alt' => 'B2in – Immobilien und Einrichtung verbinden', + 'quote' => 'Regional verwurzelt, international vernetzt – das ist B2in.', + 'author' => 'Marcel Scheibe', + ], + 'immobilien_our_values' => [ + 'title' => 'Unsere Werte', + 'subtitle' => 'Diese sechs Grundpfeiler leiten unser tägliches Handeln und definieren, wer wir als Unternehmen sind und wofür wir stehen.', + 'values' => [ + [ + 'title' => 'Persönliche Begleitung', + 'description' => 'Vom ersten Gespräch über den Kaufprozess bis zur eingerichteten Immobilie: ein Ansprechpartner, der die Märkte aus eigener Investorensicht kennt.', + 'icon' => 'light-bulb', + ], + [ + 'title' => 'Konnektivität', + 'description' => 'Wir verbinden internationale Immobilienmärkte mit lokaler Expertise und europäische Manufakturen mit den Menschen, die ihre Produkte schätzen.', + 'icon' => 'globe-alt', + ], + [ + 'title' => 'Qualität', + 'description' => 'Kompromisslose Standards – bei der Auswahl der Projekte, der Prüfung der Entwickler und der Kuration der Einrichtung.', + 'icon' => 'check-badge', + ], + [ + 'title' => 'Vertrauen', + 'description' => 'Transparente Modelle und verlässliche Partnerschaften sind unser Fundament. Hinter B2in steht ein Gesicht – und das Versprechen, dass wir liefern, was wir zusagen.', + 'icon' => 'user-group', + ], + [ + 'title' => 'Verlässlichkeit', + 'description' => 'Deutsche Vertragssicherheit trifft auf die Dynamik internationaler Märkte: klare Abläufe, geprüfte Partner, abgesicherte Zahlungswege.', + 'icon' => 'arrow-path', + ], + [ + 'title' => 'Design-Exzellenz', + 'description' => 'Design ist der Kern unserer Wertschöpfung – von internationaler Architektur bis zur kuratierten Einrichtung, die aus einer Immobilie ein Zuhause macht.', + 'icon' => 'cube-transparent', + ], + ], + ], + // Kontaktseite der Immobilien-Welt (/immobilien/kontakt). Overlay über `contact_form`: + // nur Einstieg und Betreffe weichen ab, Adresse und Beschriftungen werden geerbt. + 'contact_form_immobilien' => [ + 'hero' => [ + 'title' => 'Fragen zu einem
Projekt?', + 'subtitle' => 'Ob Exposé, Kaufprozess oder Einrichtung nach dem Kauf – schreiben Sie uns. Marcel Scheibe meldet sich persönlich bei Ihnen.', + ], + ], + 'faq_immobilien' => [ + 'title' => 'Häufige Fragen zu Immobilien', + 'subtitle' => 'Antworten zu Investments in Dubai, zum Kaufprozess und dazu, wie B2in Sie dabei begleitet.', 'questions' => [ - [ - 'question' => 'Was ist B2in und welche Services bieten Sie an?', - 'answer' => 'B2in ist die Brücke zwischen internationalen Premium-Immobilien und exklusiven Einrichtungskonzepten. Wir bieten Immobilieninvestoren Zugang zu Off-Market-Projekten (Fokus: Dubai) und begleiten sie durch den gesamten Kaufprozess. Gleichzeitig bieten wir Bauträgern ein knallhartes Supply-Chain-Management für die Beschaffung deutscher Qualitätsmöbel und bauen ein innovatives Einrichtungsnetzwerk für lokale Händler auf.', - ], - [ - 'question' => 'Was bedeutet Supply-Chain-Management bei B2in?', - 'answer' => 'Für internationale Immobilienentwickler fungieren wir als verlängerter Arm in Deutschland. Wir übernehmen die operative und strategische Steuerung der Möbel- und Innenausstattungsbeschaffung. Das bedeutet: Wir sichern Lieferverträge ab, überwachen Meilensteine direkt bei den Herstellern und eskalieren bei Abweichungen sofort auf Managementebene. Unser Ziel ist absolute Vertragssicherheit und termingerechte Lieferung ohne Reibungsverluste.', - ], - [ - 'question' => 'Wie kann ich Partner bei B2in werden?', - 'answer' => 'Unser Netzwerk richtet sich an Immobilienentwickler, Makler und den regionalen Möbelfachhandel. Wenn Sie als Entwickler deutsche Beschaffungssicherheit suchen oder als Händler Teil unseres künftigen "Local for Local"-Netzwerks werden möchten, um qualifizierte Leads von Immobilienkäufern zu erhalten, nutzen Sie einfach unser Kontaktformular. Wir prüfen jede Partnerschaft individuell auf Qualität und Passgenauigkeit.', - ], [ 'question' => 'Wie funktioniert das Immobilien-Investment über B2in?', - 'answer' => 'Wir vermitteln nicht nur, wir begleiten Sie. Marcel Scheibe ist Ihr persönlicher Berater, der den Markt (insbesondere Dubai) aus eigener Investorensicht kennt. Nach einer Bedarfsanalyse stellen wir Ihnen exklusive Projekte vor (z. B. von Azizi Developments). Der Kaufprozess selbst ist durch das staatliche Escrow-System in Dubai maximal abgesichert. Ihr besonderer Vorteil: Als B2in-Kunde erhalten Sie im Anschluss exklusiven Zugang zu unserem Netzwerk, um Ihr Investment schlüsselfertig (Turnkey) und renditeoptimiert einrichten zu lassen.', + 'answer' => 'Wir vermitteln nicht nur, wir begleiten Sie. Marcel Scheibe ist Ihr persönlicher Berater, der den Markt in Dubai aus eigener Investorensicht kennt. Nach einer Bedarfsanalyse stellen wir Ihnen ausgewählte Projekte vor, etwa von Azizi Developments. Der Kaufprozess selbst ist durch das staatliche Escrow-System in Dubai abgesichert. Ihr Vorteil als B2in-Kunde: Im Anschluss richten wir Ihre Immobilie schlüsselfertig und renditeorientiert ein.', ], [ 'question' => 'Was macht B2in zu einem vertrauenswürdigen Partner?', - 'answer' => 'Vertrauen entsteht durch Transparenz und eigene Markterfahrung. B2in-Gründer Marcel Scheibe investiert selbst in die Märkte, die wir anbieten, und ist regelmäßig vor Ort, um Baufortschritte zu prüfen. Wir verbinden die immense Dynamik internationaler Märkte (wie Dubai) mit deutscher Verlässlichkeit, striktem Vertragsmanagement und einem kuratierten Netzwerk von Premiumpartnern wie CABINET. Bei uns haben Sie immer einen persönlichen Ansprechpartner, der Ihre Interessen vertritt.', + 'answer' => 'Vertrauen entsteht durch Transparenz und eigene Markterfahrung. B2in-Gründer Marcel Scheibe investiert selbst in die Märkte, die wir anbieten, und ist regelmäßig vor Ort, um Baufortschritte zu prüfen. Wir verbinden die Dynamik internationaler Märkte mit deutscher Verlässlichkeit und einem persönlichen Ansprechpartner, der Ihre Interessen vertritt.', ], ], 'sections' => [ [ - 'title' => 'Fokus: Immobilien-Investoren (B2C)', + 'title' => 'Für Immobilien-Investoren', 'icon' => 'home-modern', 'questions' => [ [ 'question' => 'Muss ich für den Immobilienkauf persönlich nach Dubai reisen?', - 'answer' => 'Nein, der gesamte Kaufprozess kann vollständig digital und rechtsverbindlich aus der Ferne abgewickelt werden. Das Dubai Land Department bietet hierfür hochsichere Prozesse. Wir empfehlen unseren Investoren zwar gerne, sich vor Ort ein Bild von der Dynamik der Stadt zu machen, für den rechtssicheren Erwerb ist Ihre physische Anwesenheit jedoch nicht zwingend erforderlich.', + 'answer' => 'Nein, der gesamte Kaufprozess kann vollständig digital und rechtsverbindlich aus der Ferne abgewickelt werden. Das Dubai Land Department bietet hierfür sichere Prozesse. Wir empfehlen unseren Investoren zwar, sich vor Ort ein Bild von der Stadt zu machen – für den rechtssicheren Erwerb ist Ihre Anwesenheit jedoch nicht erforderlich.', ], [ 'question' => 'Was passiert nach dem Immobilienkauf? Helfen Sie bei der Einrichtung?', - 'answer' => 'Genau hier liegt der große Vorteil des B2in-Ökosystems. Unser Service endet nicht mit dem Kaufvertrag. Als B2in-Kunde erhalten Sie exklusiven Zugang zu unserem Einrichtungsnetzwerk. Wir unterstützen Sie dabei, Ihre Immobilie renditeoptimiert und schlüsselfertig („Turnkey") mit Qualitätsmöbeln auszustatten – die perfekte Grundlage für eine lukrative Kurzzeitvermietung.', + 'answer' => 'Genau hier liegt der Vorteil von B2in. Unser Service endet nicht mit dem Kaufvertrag. Wir unterstützen Sie dabei, Ihre Immobilie schlüsselfertig („Turnkey“) und renditeorientiert mit Qualitätsmöbeln auszustatten – die Grundlage für eine erfolgreiche Vermietung.', ], [ 'question' => 'Welche steuerlichen Vorteile bietet ein Investment in Dubai?', - 'answer' => 'Dubai bietet ein weltweit einzigartiges wirtschaftliches Umfeld. Es fallen weder Einkommensteuer auf Mieteinnahmen noch Kapitalertragsteuer beim Verkauf der Immobilie an. In Kombination mit den starken Mietrenditen macht dies den Standort für internationale Anleger strategisch so wertvoll.', + 'answer' => 'Dubai bietet ein besonderes wirtschaftliches Umfeld: Es fallen weder Einkommensteuer auf Mieteinnahmen noch Kapitalertragsteuer beim Verkauf der Immobilie an. In Kombination mit den Mietrenditen macht das den Standort für internationale Anleger interessant. Bitte lassen Sie Ihre persönliche steuerliche Situation von einem Steuerberater prüfen.', + ], + ], + ], + ], + ], + 'faq' => [ + 'title' => 'Häufig gestellte Fragen', + 'subtitle' => 'Antworten auf die Fragen, die uns am häufigsten gestellt werden – zur Beratung, zur Maßanfertigung und zur Ausstattung von Projekten.', + 'questions' => [ + [ + 'question' => 'Was ist B2in und welche Services bieten Sie an?', + 'answer' => 'B2in Interior steht für hochwertige Einrichtung von starken Marken – CABINET, Rotpunkt Küchen und Febrü – ergänzt um ein Netzwerk regionaler Fachexperten. Wir begleiten Privatkunden von der einzelnen Maßanfertigung bis zur kompletten Einrichtung und statten Objekte, Büros und Projektentwicklungen vollständig aus. Darüber hinaus begleiten wir ausgewählte internationale Immobilienprojekte.', + ], + [ + 'question' => 'Was bedeutet Projektausstattung bei B2in?', + 'answer' => 'Für Objekte, Büros und Projektentwicklungen übernehmen wir die vollständige Ausstattung – koordiniert aus einer Hand. Wir steuern die Beschaffung von Möbeln und Innenausstattung operativ und strategisch. Das bedeutet: Wir sichern Lieferverträge ab, überwachen Meilensteine direkt bei den Herstellern und eskalieren bei Abweichungen sofort auf Managementebene. Unser Ziel ist absolute Vertragssicherheit und termingerechte Lieferung ohne Reibungsverluste.', + ], + [ + 'question' => 'Wie kann ich Partner bei B2in Interior werden?', + 'answer' => 'Unser Netzwerk umfasst Fachbetriebe und Handwerker rund um die Einrichtung – von der Montage über das Aufmaß bis zu angrenzenden Gewerken. Wenn Sie Interesse an einer Zusammenarbeit haben, schreiben Sie uns über das Kontaktformular. Wir prüfen jede Partnerschaft einzeln, weil wir für die Arbeit unserer Partner geradestehen.', + ], + [ + 'question' => 'Wie läuft eine Beratung bei Ihnen ab?', + 'answer' => 'Am Anfang steht ein Gespräch – im Showroom, bei Ihnen zu Hause oder telefonisch. Wir schauen uns an, welche Räume es betrifft, wie Sie sie nutzen und was Ihnen wichtig ist. Danach folgt das Aufmaß vor Ort, anschließend die Planung. Sie sehen Ihre Lösung, bevor etwas gefertigt wird. Termine vereinbaren Sie über das Formular auf dieser Seite oder direkt telefonisch.', + ], + [ + 'question' => 'Was macht B2in Interior zu einem verlässlichen Partner?', + 'answer' => 'Wir arbeiten mit einer überschaubaren Zahl ausgewählter Hersteller statt mit beliebigen Katalogen – und stehen für deren Arbeit gerade. Hinter B2in Interior steht mit Marcel Scheibe ein Gesicht und ein fester Ansprechpartner, vom ersten Gespräch bis nach der Montage. Für die Gewerke rund um die Einrichtung arbeiten wir mit Fachbetrieben aus der Region zusammen, die wir kennen.', + ], + ], + 'sections' => [ + [ + 'title' => 'Für Ihr Zuhause', + 'icon' => 'home-modern', + 'questions' => [ + [ + 'question' => 'Muss ich gleich die ganze Wohnung einrichten?', + 'answer' => 'Nein. Viele Aufträge beginnen mit einem einzigen Möbelstück – einem Einbauschrank für die Dachschräge, einer neuen Küche, einem Arbeitsplatz. Ob daraus mehr wird, entscheiden Sie. Wir planen so, dass eine spätere Ergänzung passt und nicht wie ein Fremdkörper wirkt.', + ], + [ + 'question' => 'Was kostet eine Maßanfertigung im Vergleich zu Möbeln von der Stange?', + 'answer' => 'Das hängt vom Raum, den Materialien und der Ausstattung ab – eine seriöse Zahl lässt sich erst nach dem Aufmaß nennen. Was sich sagen lässt: Maßanfertigung nutzt Flächen, die ein Standardmöbel liegen lässt – Schrägen, Nischen, ungewöhnliche Höhen. Sie zahlen für den Raum, den Sie sonst verschenken. Das Angebot bekommen Sie vor der Fertigung, verbindlich und aufgeschlüsselt.', + ], + [ + 'question' => 'Übernehmen Sie auch die Montage?', + 'answer' => 'Ja. Lieferung und Montage laufen über uns beziehungsweise über Fachbetriebe aus unserem Netzwerk, mit denen wir regelmäßig arbeiten. Sie haben einen Ansprechpartner, der die Termine koordiniert – auch dann, wenn mehrere Gewerke beteiligt sind.', + ], + [ + 'question' => 'Kann ich mir die Marken vorher ansehen?', + 'answer' => 'Ja, im Showroom in Bielefeld lassen sich Oberflächen, Fronten und Beschläge in Ruhe anfassen und vergleichen. Fotos und Muster helfen bei der Vorauswahl, ersetzen aber nicht den direkten Eindruck. Vereinbaren Sie dafür am besten vorher einen Termin, damit wir uns Zeit nehmen können.', ], ], ], [ - 'title' => 'Fokus: B2B-Partner (Makler & Händler)', + 'title' => 'Für Projekte und Objekte', 'icon' => 'briefcase', 'questions' => [ [ - 'question' => 'Wie profitieren Immobilienmakler von einer Partnerschaft mit B2in?', - 'answer' => 'B2in bietet Maklern ein exklusives Werkzeug zur Kundenbindung: Sie können Ihren Immobilienkäufern den Zugang zu unserem geschlossenen Einrichtungsnetzwerk als exklusives „Closing-Geschenk" überreichen. Gleichzeitig profitieren Sie als Makler durch unser technisches Clearing-System von einer passiven Lifetime-Vergütung an den Möbelumsätzen Ihrer Kunden.', + 'question' => 'Ab welcher Größe lohnt sich eine Projektausstattung?', + 'answer' => 'Eine feste Untergrenze gibt es nicht. Sinnvoll wird die Zusammenarbeit, sobald mehrere Einheiten oder mehrere Gewerke im Spiel sind und die Koordination zum eigentlichen Aufwand wird – bei Büroflächen, möblierten Wohneinheiten oder der Ausstattung eines ganzen Objekts.', ], [ - 'question' => 'Was genau ist der „Local for Local" Marktplatz?', - 'answer' => 'Unser „Local for Local"-Prinzip ist der Gegenentwurf zu anonymen Online-Möbelgiganten. Wir machen die sofort verfügbaren Bestände und Ausstellungsstücke regionaler Fachhändler (Säule „Local Express") für einen geschlossenen Kundenkreis transparent. So stärken wir den lokalen Handel vor Ort und bieten Käufern gleichzeitig Insider-Konditionen für Premium-Einrichtung.', + 'question' => 'Wie stellen Sie Termintreue sicher?', + 'answer' => 'Wir arbeiten mit festen Lieferverträgen, verfolgen die Meilensteine direkt beim Hersteller und melden Abweichungen, sobald sie sichtbar werden – nicht erst zum Liefertermin. Bei mehreren Gewerken übernehmen wir die Abstimmung untereinander, damit die Reihenfolge auf der Baustelle stimmt.', ], [ - 'question' => 'Ist das Einrichtungsnetzwerk für jeden öffentlich zugänglich?', - 'answer' => 'Nein. Um die exklusiven Insider-Konditionen und Rabatte unserer Hersteller und lokalen Händler zu schützen, agiert der B2in-Möbelmarktplatz als „Closed Shop". Der Zugang erfolgt ausschließlich über Einladungen – beispielsweise durch unsere Partner-Makler beim Erwerb einer Immobilie.', + 'question' => 'Arbeiten Sie mit Architekten und Planungsbüros zusammen?', + 'answer' => 'Ja. Wir übernehmen bestehende Planungen und setzen sie um oder steigen früher ein, wenn Möblierung und Grundriss noch aufeinander abgestimmt werden müssen. Für Sonderlösungen sprechen wir direkt mit den Herstellern.', + ], + [ + 'question' => 'Wie werde ich Partner im regionalen Netzwerk?', + 'answer' => 'Unser Netzwerk umfasst Fachbetriebe und Handwerker rund um die Einrichtung – von der Montage bis zu angrenzenden Gewerken. Wenn Sie Interesse haben, schreiben Sie uns über das Kontaktformular. Wir prüfen jede Zusammenarbeit einzeln, weil wir für die Arbeit unserer Partner geradestehen.', ], ], ], @@ -650,14 +1058,37 @@ return [ 'subject' => 'Betreff *', 'message' => 'Ihre Nachricht *', ], - 'subjects' => [ - '' => 'Wählen Sie einen Betreff', - 'immobilien' => 'Internationale Immobilien', - 'supply_chain' => 'Supply-Chain-Management', - 'general' => 'Allgemeine Anfrage', - 'partnership' => 'Partnerschaft', - 'press' => 'Presse', - 'career' => 'Karriere', + 'subject_placeholder' => 'Wählen Sie einen Betreff', + // Betreffe in Gruppen. Die Kernmarken aus dem CMS stehen automatisch am + // Anfang der Gruppe „Interior“; die Welt der Seite bestimmt die Reihenfolge. + 'subject_groups' => [ + 'interior' => [ + 'label' => 'Interior', + 'subjects' => [ + 'beratung' => 'Beratungstermin', + 'massanfertigung' => 'Maßanfertigung für mein Zuhause', + 'kueche' => 'Küchenplanung', + 'supply_chain' => 'Ausstattung für Projekte', + ], + ], + 'immobilien' => [ + 'label' => 'Immobilien', + 'subjects' => [ + 'immobilien' => 'Beratung zum Investment', + 'expose' => 'Exposé und Verfügbarkeit', + 'kaufprozess' => 'Fragen zum Kaufprozess', + 'einrichtung' => 'Einrichtung nach dem Kauf', + ], + ], + 'b2in' => [ + 'label' => 'B2in', + 'subjects' => [ + 'partnership' => 'Zusammenarbeit im Netzwerk', + 'general' => 'Allgemeine Anfrage', + 'press' => 'Presse', + 'career' => 'Karriere', + ], + ], ], 'placeholders' => [ 'message' => 'Ihre Nachricht...', @@ -704,14 +1135,14 @@ return [ ], 'about_hero' => [ 'title' => 'Über B2in: Unsere Mission', - 'quote' => '"Meine Mission ist es, zwei Welten zu verbinden, die zusammengehören: internationale Immobilien und exklusive Einrichtung. B2in gibt dem lokalen Fachexperten die digitalen Werkzeuge und den Immobilienentwickler den operativen Partner vor Ort.

Wir bauen Brücken – zwischen europäischem Design, internationalen Märkten und dem Zuhause der Menschen."', + 'quote' => '"Gute Einrichtung entsteht nicht im Katalog, sondern im Gespräch. Deshalb arbeiten wir mit wenigen, ausgewählten Herstellern, die wir kennen – und mit Fachbetrieben aus der Region, für deren Arbeit wir geradestehen.

Mein Anspruch ist einfach: ein Ansprechpartner, vom ersten Aufmaß bis nach der Montage."', 'founder_name' => 'Marcel Scheibe', 'founder_title' => 'Gründer & CEO, B2in', 'image' => 'b2in/about-hero.jpg', 'image_alt' => 'Marcel Scheibe, Gründer und CEO von B2in', 'card_title' => 'B2in', - 'card_text' => 'Connecting Design and Property', + 'card_text' => 'Premium-Einrichtung. Persönlich gemacht.', ], 'broker_section' => [ @@ -988,7 +1419,7 @@ return [ ], 'magazin_list' => [ 'title' => 'B2in Magazin', - 'subtitle' => 'Insights, Marktanalysen und Praxiswissen rund um Immobilien-Investments in Dubai, Einrichtungskonzepte und das B2in-Ökosystem.', + 'subtitle' => 'Praxiswissen rund ums Einrichten: Maßanfertigung, Küchenplanung, Marken und der Möbelhandel aus der Region.', 'read_more' => 'Weiterlesen', 'load_more' => 'Weitere Artikel laden', ], @@ -997,26 +1428,26 @@ return [ 'timeline' => [ [ 'title' => 'Die Idee', - 'description' => '2024 erkannten wir eine entscheidende Lücke: Der lokale Fachhandel braucht digitale Sichtbarkeit und Immobilienkäufer suchen nach nahtlosen Einrichtungslösungen. Diese beiden Welten gehören zusammen.', + 'description' => '2024 war der Ausgangspunkt eine einfache Beobachtung: Wer hochwertig einrichten will, findet online viele Kataloge – aber selten jemanden, der zuhört, aufmisst und am Ende geradesteht.', 'icon' => 'light-bulb', ], [ 'title' => 'Das Fundament', - 'description' => 'Wir entwickeln die B2in-Plattform: ein Ökosystem, das lokale Einrichtungsexperten, europäische Hersteller und Immobilienprofis auf einer gemeinsamen Plattform verbindet – fair, transparent und technologiegestützt.', + 'description' => 'Wir bauen das Netzwerk auf: ausgewählte Hersteller, Fachbetriebe und Handwerker aus der Region – verbunden durch dieselbe Grundhaltung, dass Qualität und Wort halten zusammengehören.', 'icon' => 'rocket-launch', ], [ 'title' => 'Die Erweiterung', - 'description' => '2025/2026 erweitert B2in sein Ökosystem: Internationale Immobilien und Supply-Chain-Management für Entwickler werden zur dominanten Säule. Der lokale Möbelmarktplatz bleibt als starker ergänzender Bereich bestehen.', + 'description' => '2026 rückt das Interior in den Mittelpunkt: Mit CABINET, Rotpunkt Küchen und Febrü stehen starke Kernmarken im Zentrum, ergänzt um ein Netzwerk regionaler Fachexperten. Internationale Immobilien bleiben als ergänzender Bereich bestehen.', 'icon' => 'globe-alt', ], [ 'title' => 'Die Vision', - 'description' => 'B2in wird zum zentralen Netzwerk für "Design & Property" – regional verwurzelt, international vernetzt. In jeder Region der verlässliche Partner für Immobilien-Investments und exklusive Einrichtungskonzepte.', + 'description' => 'B2in Interior wird in der Region die erste Adresse für hochwertige Einrichtung – vom einzelnen Einbauschrank bis zur vollständigen Ausstattung. Regional verwurzelt, international vernetzt.', 'icon' => 'star', ], ], - 'summary' => 'Was als Vision begann, den lokalen Handel zu stärken, ist heute ein Netzwerk, das Immobilien und Einrichtung nahtlos verbindet. B2in schließt die Lücke zwischen internationalen Investments und lokaler Expertise – mit Marcel Scheibe als Gesicht und Ansprechpartner.', + 'summary' => 'Aus der Idee, hochwertige Einrichtung wieder persönlich zu machen, ist ein Netzwerk aus starken Marken und regionalen Fachbetrieben geworden – mit Marcel Scheibe als Gesicht und festem Ansprechpartner.', ], 'about_image_break' => [ 'image' => 'b2in/best-of-two-worlds.jpg', @@ -1026,37 +1457,37 @@ return [ ], 'our_values' => [ 'title' => 'Unsere Werte', - 'subtitle' => 'Diese sechs Grundpfeiler leiten unser tägliches Handeln und definieren, wer wir als Unternehmen sind und wofür wir stehen.', + 'subtitle' => 'Sechs Grundsätze, an denen wir uns messen lassen – in der Beratung, in der Planung und nach der Montage.', 'values' => [ [ - 'title' => 'Innovation', - 'description' => 'Wir entwickeln digitale Lösungen, die Immobilienprofis und lokale Einrichtungsexperten gleichermaßen einen echten Wettbewerbsvorteil verschaffen.', - 'icon' => 'light-bulb', + 'title' => 'Beratung statt Verkauf', + 'description' => 'Wir fragen zuerst, wie Sie Ihre Räume nutzen, und schlagen erst danach etwas vor. Manchmal ist die richtige Antwort ein kleineres Möbelstück als gedacht.', + 'icon' => 'chat-bubble-left-right', ], [ - 'title' => 'Konnektivität', - 'description' => 'Wir verbinden internationale Immobilienmärkte mit lokaler Expertise und europäische Manufakturen mit den Menschen, die ihre Produkte schätzen.', - 'icon' => 'globe-alt', + 'title' => 'Maßarbeit', + 'description' => 'Schrägen, Nischen und ungewöhnliche Höhen sind kein Problem, sondern der Grund für eine Maßanfertigung. Wir planen den Raum, den ein Standardmöbel liegen lässt.', + 'icon' => 'cube-transparent', ], [ - 'title' => 'Qualität', - 'description' => 'Kompromisslose Standards – bei der Auswahl unserer Partner, der Kuration der Einrichtung, der Überwachung von Lieferketten und der Technologie, die alles zusammenhält.', + 'title' => 'Ausgewählte Marken', + 'description' => 'Lieber wenige Hersteller, die wir kennen, als ein beliebiges Sortiment. Für die Marken, die wir führen, stehen wir gerade.', 'icon' => 'check-badge', ], [ - 'title' => 'Vertrauen', - 'description' => 'Transparente Modelle und verlässliche Partnerschaften sind unser Fundament. Hinter B2in steht ein Gesicht – und das Versprechen, dass wir liefern, was wir zusagen.', + 'title' => 'Regional verwurzelt', + 'description' => 'Wir arbeiten mit Fachbetrieben und Handwerkern aus der Region – Menschen, die wir kennen und die vor Ort sind, wenn es darauf ankommt.', + 'icon' => 'map-pin', + ], + [ + 'title' => 'Ein Ansprechpartner', + 'description' => 'Vom ersten Gespräch über das Aufmaß bis nach der Montage bleibt der Kontakt derselbe. Auch dann, wenn mehrere Gewerke beteiligt sind.', 'icon' => 'user-group', ], [ - 'title' => 'Nachhaltigkeit', - 'description' => 'Wir stärken den lokalen Handel, optimieren Transportwege und sorgen dafür, dass internationale Beschaffung verantwortungsvoll und effizient abläuft.', - 'icon' => 'arrow-path', - ], - [ - 'title' => 'Design-Exzellenz', - 'description' => 'Design ist der Kern unserer Wertschöpfung – von internationaler Architektur über kuratierte Einrichtung bis zur intuitiven Gestaltung unserer digitalen Plattform.', - 'icon' => 'cube-transparent', + 'title' => 'Verlässlichkeit', + 'description' => 'Zusagen zu Terminen und Preisen halten wir ein. Wenn sich etwas verschiebt, sagen wir es, sobald wir es wissen – nicht erst am Liefertag.', + 'icon' => 'shield-check', ], ], ], @@ -1478,12 +1909,80 @@ return [ ], ], 'articles' => [ + // ⚠️ Platzhalter-Beiträge zum neuen Interior-Auftritt. Inhaltlich bewusst allgemein + // gehalten (keine Referenzen, keine Zahlen, keine Namen) – vor dem Livegang von Marcel + // gegenlesen und bei Bedarf um echte Beispiele ergänzen. + 6 => [ + 'id' => 6, + 'slug' => 'massanfertigung-warum-der-millimeter-zaehlt', + 'title' => 'Nach Maß statt von der Stange:
Warum der Millimeter über den Raum entscheidet', + 'subtitle' => 'Dachschrägen, Nischen, ungewöhnliche Höhen: Wo Standardmöbel aufgeben, fängt die Maßanfertigung erst an – und macht aus totem Raum nutzbare Fläche.', + 'image' => 'b2in/stileigentum.jpg', + 'category' => 'Einrichtung', + 'date' => 'August 26, 2026', + 'readTime' => '5 min read', + 'author' => [ + 'name' => 'Marcel Scheibe', + 'bio' => 'Gründer & CEO von B2in Interior. Mit Wurzeln in der Möbelbranche begleitet er Kunden von der Planung bis nach der Montage.', + 'avatar' => 'b2in/marcel-scheibe.jpg', + ], + 'content' => [ + 'intro' => 'Fast jede Wohnung hat sie: die Ecke, in der nichts richtig passt. Die Schräge unter dem Dach, die Nische neben dem Schornstein, der Flur, der für einen Schrank zu schmal und für nichts zu breit ist. Wer dort ein Standardmöbel hinstellt, verliert doppelt – vorne bleibt ein Spalt, hinten bleibt Luft. Maßanfertigung setzt genau da an, und der Unterschied entsteht nicht am Möbel, sondern lange vorher: beim Aufmaß.', + 'sections' => [ + [ + 'title' => 'Aufmaß ist keine Formalie', + 'content' => 'Wände sind selten gerade, Böden selten eben, und in Altbauten weicht die Höhe über die Länge eines Raums gern um mehrere Zentimeter ab. Ein Aufmaß vor Ort nimmt diese Abweichungen auf, statt sie zu ignorieren. Erst danach lässt sich planen, was am Ende auf den Millimeter passt – ohne Blenden, die etwas kaschieren müssen, und ohne Fugen, in denen sich Staub sammelt.', + ], + [ + 'title' => 'Der Raum, den man sonst verschenkt', + 'content' => 'Bei einer Dachschräge liegt der Gewinn nicht in der Optik, sondern in der Fläche. Ein Schrank, der der Schräge folgt, nutzt den Bereich, den ein gerader Korpus abschneidet – je nach Neigung ist das ein erheblicher Teil des Stauraums. Ähnlich in Nischen: Ein eingepasster Einbau bringt Tiefe, die ein freistehendes Möbel nie erreicht, weil es Abstand zur Wand braucht.', + ], + [ + 'title' => 'Wo sich Maß nicht lohnt', + 'content' => 'Ehrlich bleibt, wer auch die Gegenseite nennt: Bei geraden Wänden, üblichen Raumhöhen und Standardmaßen ist ein gutes Serienmöbel oft die vernünftigere Wahl – schneller lieferbar und günstiger. Die Frage lautet deshalb nicht „Maß oder nicht", sondern: Wo im Raum gibt es tatsächlich etwas zu gewinnen? Diese Antwort gehört an den Anfang der Beratung, nicht ans Ende.', + ], + ], + ], + ], + 7 => [ + 'id' => 7, + 'slug' => 'kueche-planen-vom-wunsch-zum-grundriss', + 'title' => 'Küche planen:
Wie aus Wünschen ein Grundriss wird', + 'subtitle' => 'Bevor über Fronten und Farben gesprochen wird, entscheidet sich der Erfolg einer Küche an drei Fragen – und die haben mit Design zunächst wenig zu tun.', + 'image' => 'b2in/style2own.jpg', + 'category' => 'Küche', + 'date' => 'August 20, 2026', + 'readTime' => '6 min read', + 'author' => [ + 'name' => 'Marcel Scheibe', + 'bio' => 'Gründer & CEO von B2in Interior. Mit Wurzeln in der Möbelbranche begleitet er Kunden von der Planung bis nach der Montage.', + 'avatar' => 'b2in/marcel-scheibe.jpg', + ], + 'content' => [ + 'intro' => 'Die meisten Küchengespräche beginnen bei der Front: matt oder glänzend, hell oder dunkel, Griff oder grifflos. Verständlich, denn das sieht man. Nur entscheidet sich an dieser Stelle noch gar nichts. Ob eine Küche im Alltag funktioniert, hängt an Fragen, die sich vor der ersten Farbmustertafel klären lassen – und die man später kaum noch korrigieren kann.', + 'sections' => [ + [ + 'title' => 'Frage eins: Wie kochen Sie wirklich?', + 'content' => 'Nicht, wie Sie gern kochen würden – wie Sie es tatsächlich tun. Wer täglich frisch kocht, braucht Arbeitsfläche neben dem Herd und kurze Wege zwischen Kühlschrank, Spüle und Kochfeld. Wer die Küche vor allem als Treffpunkt nutzt, plant anders: mehr Platz zum Stehen, eine Fläche, an der jemand sitzen kann, während gekocht wird. Beides sind gute Küchen, aber es sind nicht dieselben.', + ], + [ + 'title' => 'Frage zwei: Was gibt der Raum vor?', + 'content' => 'Fenster, Türen, Anschlüsse und tragende Wände bestimmen den Rahmen, bevor irgendjemand etwas plant. Manches lässt sich verlegen, manches nicht – und was es kostet, sollte auf dem Tisch liegen, bevor die Planung darauf aufbaut. Auch die Deckenhöhe gehört dazu: Sie entscheidet, ob Oberschränke bis nach oben laufen können oder ob darüber eine Staubkante entsteht.', + ], + [ + 'title' => 'Frage drei: Was soll in zehn Jahren noch stimmen?', + 'content' => 'Eine Küche bleibt lange. Fronten lassen sich in vielen Systemen später tauschen, Korpusmaße und Grundriss praktisch nicht. Es lohnt sich deshalb, bei Auszügen, Beschlägen und Arbeitsplatte auf Haltbarkeit zu achten und die Aufmerksamkeit für Modisches auf die Teile zu lenken, die austauschbar sind. Erst danach kommt die Farbe – und dann darf sie auch mutig sein.', + ], + ], + ], + ], 1 => [ 'id' => 1, 'title' => 'Sicherheit statt Bürokratie:
Warum das Dubai Escrow-System deutsche Investoren überrascht', 'subtitle' => 'Wie das staatlich regulierte Treuhandsystem in Dubai Investorengelder konsequent schützt – und warum es internationalen Standards weit voraus ist.', 'image' => 'b2in/magazin-1.jpg', 'category' => 'Dubai Investment', + 'world' => 'immobilien', 'date' => 'März 10, 2026', 'readTime' => '6 min read', 'author' => [ @@ -1515,6 +2014,7 @@ return [ 'subtitle' => 'Strategische Waterfront-Location, hohe Mietnachfrage und enormes Potenzial für Wertsteigerung – Al Jaddaf ist der Hidden Champion unter Dubais Vierteln.', 'image' => 'b2in/magazin-2.jpg', 'category' => 'Dubai Investment', + 'world' => 'immobilien', 'date' => 'März 5, 2026', 'readTime' => '7 min read', 'author' => [ @@ -1546,6 +2046,7 @@ return [ 'subtitle' => 'Warum schlüsselfertige Einrichtungskonzepte den Unterschied zwischen durchschnittlicher und Premium-Rendite ausmachen.', 'image' => 'b2in/magazin-3.jpg', 'category' => 'Rendite & Einrichtung', + 'world' => 'immobilien', 'date' => 'Februar 25, 2026', 'readTime' => '6 min read', 'author' => [ diff --git a/resources/lang/de/ui.php b/resources/lang/de/ui.php index dde54685..6508056f 100644 --- a/resources/lang/de/ui.php +++ b/resources/lang/de/ui.php @@ -16,9 +16,90 @@ return [ 'menu_close' => 'Menü schließen', 'menu_open' => 'Menü öffnen', + // Welten-Wechsler (Interior / Immobilien) + // Farb-Schalter für die Abstimmung der Palette (Nachtrag 18) + 'palette' => [ + 'label' => 'Farbwelt', + 'classic' => 'Bisher', + 'modern' => 'Neu', + 'draft' => 'Entwurf', + ], + // Slideshow im Hero des Entwurfs (Nachtrag 19) + 'hero_slider' => [ + 'label' => 'Einblicke in unsere Marken', + 'dots' => 'Bild wählen', + 'dot' => 'Bild :n von :total: :label', + 'empty' => 'Noch keine Bilder hinterlegt.', + ], + 'worlds_label' => 'Bereich wechseln', + 'world_interior' => 'Interior', + 'world_immobilien' => 'Immobilien', + + 'display_feed_title' => 'Live-Ansicht des Showroom-Displays', + 'references_empty' => 'Die ersten Projekte erscheinen hier, sobald wir sie zeigen dürfen.', + + // Terminanfrage-Widget + 'appointment' => [ + 'name' => 'Name', + 'email' => 'E-Mail', + 'phone' => 'Telefon', + 'topic' => 'Anliegen', + 'topic_placeholder' => 'Bitte auswählen', + 'preferred_date' => 'Wunschtermin', + 'message' => 'Nachricht', + 'message_placeholder' => 'Worum geht es? Gern in zwei Sätzen.', + 'submit' => 'Anfrage senden', + 'sending' => 'Wird gesendet …', + 'success' => 'Vielen Dank! Wir melden uns zeitnah bei Ihnen und stimmen den Termin ab.', + 'new_request' => 'Weitere Anfrage', + 'date_in_past' => 'Bitte einen Termin ab heute wählen.', + 'whatsapp_intro' => 'Am schnellsten per WhatsApp – wir bereiten die Nachricht mit Ihren Angaben vor.', + 'or_form' => 'oder per Formular', + 'whatsapp_text' => 'Hallo B2in Interior, ich hätte gern einen Beratungstermin.', + 'whatsapp_note' => 'Öffnet WhatsApp mit vorbereitetem Text. Abgeschickt wird die Nachricht erst von Ihnen.', + ], + + // Kernmarken + 'all_brands_cta' => 'Alle Marken ansehen', + 'brands' => 'Marken', + 'whatsapp' => 'Per WhatsApp schreiben', + 'all_brands' => 'Alle Marken', + 'brand_services' => 'Leistungen und Besonderheiten', + 'brand_cta_title' => 'Interesse an :name?', + 'brand_cta_text' => 'Vereinbaren Sie einen Termin – wir beraten Sie persönlich und unverbindlich.', + 'request_appointment' => 'Termin anfragen', + 'brand_website' => 'Zur Herstellerseite', + 'other_brands' => 'Unsere weiteren Kernmarken', + 'more_about' => 'Mehr zu :name', + + // Markengalerie – realisierte Projekte vs. Entwuerfe + 'brand_gallery_title' => 'Inspirationen und Entwürfe', + 'brand_gallery_intro' => 'Visualisierungen, die zeigen, was in einem Raum möglich ist – noch bevor das erste Möbel steht.', + // Varianten je Galerie-Inhalt (Nachtrag 19): nur Entwürfe / nur Projekte und Werkstatt / gemischt + 'brand_gallery_title_inspirations' => 'Inspirationen und Entwürfe', + 'brand_gallery_intro_inspirations' => 'Visualisierungen, die zeigen, was in einem Raum möglich ist – noch bevor das erste Möbel steht.', + 'brand_gallery_title_projects' => 'Referenzen', + 'brand_gallery_intro_projects' => 'Realisierte Projekte – so sieht es aus, wenn Planung und Handwerk fertig sind.', + 'brand_gallery_title_projects_company' => 'Referenzen und Einblicke', + 'brand_gallery_intro_projects_company' => 'Realisierte Projekte und ein Blick in die Werkstatt, in der sie entstehen.', + 'brand_gallery_title_mixed' => 'Projekte und Entwürfe', + 'brand_gallery_intro_mixed' => 'Umgesetzte Projekte und Visualisierungen, die zeigen, was in einem Raum möglich ist.', + 'brand_gallery_tab_projects' => 'Realisierte Projekte', + 'brand_gallery_tab_inspirations' => 'Inspirationen und Entwürfe', + 'brand_gallery_badge_project' => 'Realisiertes Projekt', + 'brand_gallery_badge_inspiration' => 'Visualisierung', + 'brand_gallery_badge_company' => 'Aus der Werkstatt', + 'brand_gallery_inspiration_note' => 'Entwürfe und Visualisierungen. Sie zeigen gestalterische Möglichkeiten, noch bevor das erste Möbel steht.', + 'previous' => 'Vorheriges Bild', + 'next' => 'Nächstes Bild', + // Footer 'founder_ceo' => 'Gründer & CEO', 'legal_notice' => 'Impressum', + 'privacy_policy' => 'Datenschutz', + 'terms_of_service' => 'AGB', + 'cookie_policy' => 'Cookie-Richtlinie', + 'faq' => 'Häufige Fragen', 'cookie_settings' => 'Cookie-Einstellungen', // Announcement Bar @@ -34,6 +115,7 @@ return [ // Contact Form 'contact_form' => [ + 'subject_placeholder' => 'Wählen Sie einen Betreff', 'success_title' => 'Vielen Dank!', 'success_message' => 'Ihre Nachricht ist bei uns eingegangen. Wir melden uns schnellstmöglich bei Ihnen.', 'first_name' => 'Vorname *', diff --git a/resources/lang/en/b2in.php b/resources/lang/en/b2in.php index 858c0641..a9b7e491 100644 --- a/resources/lang/en/b2in.php +++ b/resources/lang/en/b2in.php @@ -5,45 +5,108 @@ return [ 'b2in' => [ 'announcement_bar' => [ 'enabled' => true, - 'id' => 'azizi-launch-2026', - 'badge' => 'NEW LAUNCH', - 'text' => 'Azizi Creek Views 4 – Exclusive Off-Market Project in Dubai '.\App\Helpers\PriceHelper::formatAed(1_125_000, 'from'), + 'id' => 'interior-rotpunkt-2026', + 'badge' => 'NEW', + 'text' => 'Rotpunkt Küchen now available at B2in Interior – book your consultation', + 'link_text' => 'Request an appointment', + 'link_url' => '/contact', + ], + 'announcement_bar_immobilien' => [ + 'enabled' => true, + 'id' => 'azizi-creek-views-4-2026', + 'badge' => 'NEW', + 'text' => 'Azizi Creek Views 4 – Off-Market Project in Dubai from AED 1,125,000', 'link_text' => 'View Exposé', 'link_url' => '/immobilien/azizi-creek-views-4', ], + 'appointment_widget' => [ + 'button_label' => 'Request an appointment', + 'title' => 'Request an appointment', + 'intro' => 'Tell us briefly what it is about – we will get back to you and arrange a date. Personal, without obligation.', + 'success' => 'Thank you! We will get back to you shortly to arrange the appointment.', + 'whatsapp_text' => 'Hello B2in Interior, I would like to arrange an appointment.', + ], + 'referenzen_hero' => [ + 'title' => 'Our references', + 'text' => 'Here we show projects we have actually carried out – no drafts and no visualisations. The section grows with every project we are allowed to show.', + 'image' => '', + 'image_alt' => 'B2in Interior – completed projects', + ], + 'reference_projects' => [ + 'title' => 'Completed projects', + 'text' => 'Work carried out for our clients, sorted by brand. Drafts and visualisations are deliberately kept separate, on the brand pages.', + 'empty' => 'The first projects are under way and will appear here as soon as we are allowed to show them. Until then, we are happy to look at what is possible for your rooms.', + ], + 'display_feed' => [ + 'title' => 'Live from our showroom', + 'text' => 'Whatever is currently running on the screen in the showroom is shown here live – the same view, the same state.', + 'note' => 'The content changes automatically.', + 'link_text' => 'See the references', + 'link_url' => '/referenzen', + 'iframe_title' => 'Live view of the showroom display', + 'display_id' => '', + ], + 'footer' => [ + 'claim' => 'Premium interiors. Personally delivered.', + 'byline' => 'Marcel Scheibe', + 'links' => [ + ['label' => 'FAQ', 'url' => '/faq'], + ['label' => 'Privacy Policy', 'url' => '/privacy'], + ['label' => 'Terms of Service', 'url' => '/terms'], + ['label' => 'Cookie Policy', 'url' => '/cookie-policy'], + ['label' => 'Legal Notice', 'url' => '/impressum'], + ], + ], 'header' => [ 'portal_login' => 'Partner Login', 'navigation' => [ ['label' => 'Home', 'url' => '/'], - ['label' => 'Real Estate', 'url' => '/immobilien'], - ['label' => 'Network', 'url' => '/netzwerk'], + ['label' => 'Brands', 'url' => '/marken'], + ['label' => 'References', 'url' => '/referenzen'], ['label' => 'Magazine', 'url' => '/magazin'], - ['label' => 'FAQ', 'url' => '/faq'], ['label' => 'About B2in', 'url' => '/about'], ], ], - 'hero' => [ - 'title' => 'B2in – Where Exclusive Real Estate Investments Meet Smart Interior Design.', - 'subtitle' => 'Your partner for high-yield international real estate and global supply chain management in the interior sector.', - 'image' => 'b2in/hero-room.jpg', - 'image_alt' => 'B2in – International Real Estate and Exclusive Interior Concepts', - 'cta1_text' => 'View Real Estate Projects', - 'cta1_link' => '/immobilien', - 'cta2_text' => 'Our Network', - 'cta2_link' => '/netzwerk', - 'stats' => [ - 'International Real Estate', - 'Exclusive Furnishing', - 'Personal Consulting', + 'header_immobilien' => [ + 'portal_login' => 'Partner Login', + 'navigation' => [ + ['label' => 'Real Estate', 'url' => '/immobilien'], + ['label' => 'Projects', 'url' => '/immobilien#projekte'], + ['label' => 'Magazine', 'url' => '/immobilien/magazin'], + ['label' => 'About B2in', 'url' => '/immobilien/ueber-b2in'], ], - 'card_title' => 'B2in', - 'card_text' => 'Connecting Design and Property', + ], + 'hero' => [ + 'title' => 'B2in Interior – Premium furnishing from strong brands. For your home. For your project.', + 'subtitle' => 'From a single bespoke piece to a complete fit-out – with CABINET, Rotpunkt Küchen, Febrü and a network of regional specialists.', + 'image' => 'b2in/hero-room.jpg', + 'image_alt' => 'B2in Interior – Premium furnishing from strong brands', + 'cta1_text' => 'Discover our brands', + 'cta1_link' => '/marken', + 'cta2_text' => 'Request an appointment', + 'cta2_link' => '/contact', + 'stats' => [ + 'Premium brands', + 'Bespoke solutions', + 'Personal consulting', + ], + 'card_title' => 'B2in Interior', + 'card_text' => 'Interiors that last.', + ], + 'hero_slider' => [ + 'eyebrow' => 'B2in Interior', + 'title' => 'Rooms from a single source. With strong brands and personal solutions.', + 'subtitle' => 'We bring together CABINET, Rotpunkt Küchen, Febrü and a network of regional experts to create individual interior solutions for your home or your project.', + 'cta_text' => 'Discover our brands', + 'cta_link' => '/marken', + 'interval' => 7, + 'slides' => [], ], 'founder_bar' => [ 'image' => 'b2in/marcel-scheibe.jpg', 'name' => 'Marcel Scheibe', 'title' => 'Founder & CEO, B2in', - 'statement' => 'B2in by Marcel Scheibe – Your personal partner for international real estate and exclusive interior concepts.', + 'statement' => 'B2in by Marcel Scheibe – Your personal partner for high-quality furnishing and strong brands.', ], 'synergie_section' => [ 'title' => 'Two Worlds. One Network.', @@ -81,8 +144,8 @@ return [ 'vision_section' => [ 'title' => 'Built on Expertise and Trust', 'paragraphs' => [ - 'B2in (Bridges2international) connects two worlds: international real estate and exclusive interior concepts. As your personal partner, I guide you through both areas – with expertise, network, and the commitment that every decision is based on trust.', - 'Whether an investment in Dubai, a villa in Lisbon, or the bespoke furnishing of your new home by local specialists – at B2in, everything comes together.', + 'B2in originally stood for Bridges to International. Today it stands just as much for Bridges to Interior: high-quality furnishing with a personal signature. As your contact, I connect selected manufacturers, regional specialists and a dependable network – so that rooms become places that last.', + 'Whether a bespoke fitted wardrobe, a new kitchen or the complete fit-out of a property: at B2in, everything comes together. And anyone thinking beyond the interior will also find access to international real estate here.', 'Locally rooted, internationally connected – that is B2in.', ], 'image' => 'b2in/marcel-scheibe-about.jpg', @@ -132,12 +195,209 @@ return [ 'image_alt' => 'The result for the customer – Property and interior from one source', 'image_caption' => 'Property and interior – from one source', ], + 'audience_switch' => [ + 'title' => 'For your home. For your project.', + 'subtitle' => 'Whether a single piece of furniture or a complete fit-out – we accompany you with the same brands, the same standards and one personal contact.', + 'cards' => [ + [ + 'icon' => 'home', + 'title' => 'Furnishing for your home', + 'description' => 'Single pieces, bespoke built-in solutions or a complete interior: our core brands and our specialist retail network accompany you from the first idea to installation. Visit our showrooms or arrange a personal consultation.', + 'button_text' => 'Request a consultation', + 'button_link' => '/contact', + ], + [ + 'icon' => 'building-office-2', + 'title' => 'Fit-out for your project', + 'description' => 'Complete furnishing for properties, offices and developments – coordinated from a single source, from the kitchen to the last flower. With a single German contract partner and an established manufacturer network.', + 'button_text' => 'Discuss your project', + 'button_link' => '/contact', + ], + ], + ], + 'core_brands' => [ + 'title' => 'Our core brands', + 'subtitle' => 'We do not work with arbitrary catalogues, but with selected manufacturers we stand behind – each brand a specialist in its field.', + 'brands' => [ + [ + 'slug' => 'cabinet', + 'name' => 'CABINET', + 'tagline' => 'Fitted wardrobes made to measure.', + 'teaser' => 'Bespoke wardrobe solutions, planned to the millimetre – built for rooms where standard will not do. Experience the possibilities in our Bielefeld showroom.', + 'image' => '', + 'logo' => '', + 'link_text' => 'More about CABINET', + 'origin_city' => 'Cologne', + 'origin_distance' => 'about 200 km', + 'origin_role' => 'Made in the Cologne manufactory, advised and planned at the CABINET Store Bielefeld.', + ], + [ + 'slug' => 'rotpunkt', + 'name' => 'Rotpunkt Küchen', + 'tagline' => 'Kitchens with character.', + 'teaser' => 'Premium kitchens made in Germany – individually planned, sustainably conceived and built for the heart of your home. Consultation and planning in our kitchen studio.', + 'image' => '', + 'logo' => '', + 'link_text' => 'More about Rotpunkt', + 'origin_city' => 'Bünde', + 'origin_distance' => 'about 30 km', + 'origin_role' => 'Kitchens from their own production in Bünde – planned with us in the kitchen studio.', + ], + [ + 'slug' => 'februe', + 'name' => 'Febrü', + 'tagline' => 'Work where you like to be.', + 'teaser' => 'Furniture for focused work – from a well-considered home office to a complete office fit-out. Ergonomics and design for home and company alike.', + 'image' => '', + 'logo' => '', + 'link_text' => 'More about Febrü', + 'origin_city' => 'Herford', + 'origin_distance' => 'about 20 km', + 'origin_role' => 'Office and contract furniture from their own production in Herford.', + ], + [ + 'slug' => 'tischlerei', + 'name' => 'Otterpohl Möbelservice', + 'tagline' => 'Not easy click & buy. But still easy to get.', + 'teaser' => 'Bespoke furniture, doors, stairs, kitchens, wall cladding and contract work – up to complete interior fit-outs. Joinery from Rheda-Wiedenbrück.', + 'image' => '', + 'logo' => 'marken/otterpohl.png', + 'link_text' => 'More about Otterpohl', + 'origin_city' => 'Rheda-Wiedenbrück', + 'origin_distance' => 'about 35 km', + 'origin_role' => 'Joinery and interior fit-out from Rheda-Wiedenbrück – from a single piece to the whole room, installation included.', + ], + ], + ], + 'origin_section' => [ + 'title' => 'Made in the region', + 'subtitle' => 'Four German manufactories, three of them in East Westphalia. Short distances, people we know, and quality you can go and see.', + 'map_alt' => 'Schematic map: Bielefeld as the starting point, production in Bünde, Herford, Rheda-Wiedenbrück and Cologne', + 'region_label' => 'East Westphalia', + 'distance_suffix' => 'from Bielefeld', + 'hub' => [ + 'city' => 'Bielefeld', + 'role' => 'B2in Interior · consulting, planning, showrooms', + 'x' => 400, + 'y' => 175, + ], + 'link_text' => 'To the brand', + 'steps_title' => 'Made in Cologne, planned in Bielefeld', + 'steps_text' => 'Take CABINET: the furniture is made in Cologne. Consulting, measuring and planning happen with us at the CABINET Store Bielefeld. Assembly takes place on site – with our joinery, which on request also builds what the catalogue does not offer.', + 'steps' => [ + [ + 'title' => 'Consulting and planning in Bielefeld', + 'description' => 'At the CABINET Store we take measurements, plan down to the last detail and agree every front and interior layout with you.', + ], + [ + 'title' => 'Production in Cologne', + 'description' => 'CABINET builds your wardrobe in its own manufactory – to the measurements from the planning.', + ], + [ + 'title' => 'Assembly on site, special requests included', + 'description' => 'Otterpohl assembles and adds what the room needs beyond that: panels, connections, custom solutions.', + ], + ], + ], + 'brand_craft' => [ + 'title' => 'Planning plus craftsmanship', + 'paragraphs' => [ + 'CABINET, Rotpunkt and Febrü already cover an enormous range of planning and design options. So this is explicitly not about custom-made pieces at any price.', + 'Still, there is always that one case where even an excellent system reaches its limit: the transition to the architecture, a special cladding, the wall that is still missing, or the last millimetre. That is where our collaboration with Otterpohl from Rheda-Wiedenbrück comes in – joinery, interior fit-out and installation from a single source.', + 'We weigh design value, technical necessity and economic sense honestly. In the end, your wishes decide – not our urge to sell you yet another special solution.', + ], + 'detail' => [ + 'title' => 'When craftsmanship takes over the last millimetre', + 'paragraphs' => [ + 'Possible are, for example, precisely fitted alcove solutions, special claddings, full-height built-ins, transitions to adjoining furniture, individual lacquer finishes and real-wood veneers, or adaptations to the existing architecture.', + 'If needed, the concept does not end at the furniture. Drywall, wallpaper, flooring, wall design, lighting and further details can be included.', + ], + ], + 'claim' => 'The possibilities of a made-to-measure system. The flexibility of craftsmanship.', + 'link_text' => 'More about Otterpohl', + 'brand_slug' => 'tischlerei', + ], + 'sponsoring' => [ + 'title' => 'Rooted in the region', + 'subtitle' => 'We support clubs and organisations from our region – because being rooted is more than an address.', + 'logos' => [ + [ + 'name' => 'DSC Arminia Bielefeld', + 'image' => 'sponsoring/arminia-bielefeld.png', + 'text' => 'A long-standing club from Bielefeld – the city we call home.', + ], + [ + 'name' => 'TC Bad Waldliesborn-Horn', + 'image' => 'sponsoring/tc-bad-waldliesborn-horn.jpg', + 'text' => 'Tennis club in Bad Waldliesborn. Club sport that brings people together locally.', + ], + [ + 'name' => 'TC Hövelhof', + 'image' => 'sponsoring/tc-hoevelhof.png', + 'text' => 'Tennis club in Hövelhof – grassroots sport in the Paderborn district.', + ], + ], + ], + 'immobilien_teaser' => [ + 'title' => 'Also: international real estate', + 'text' => 'Alongside interiors, we accompany selected international real estate projects – personally curated and with a clear eye for substance.', + 'link_text' => 'To the properties', + 'link_url' => '/immobilien', + ], + 'marken_hero' => [ + 'title' => 'Our brands', + 'text' => 'We do not work with arbitrary catalogues, but with selected manufacturers we stand behind. Each brand is a specialist in its field – and each brings what good interiors are made of: visible quality and advice that listens.', + 'image' => 'b2in/b2a.jpg', + 'image_alt' => 'B2in Interior – our core brands', + ], + 'marken_network' => [ + 'title' => 'More than brands: our network', + 'paragraphs' => [ + 'Good interiors do not end with the furniture. That is why we work with specialist firms and craftspeople from the region – from measuring up and installation to adjacent trades.', + 'For you that means one point of contact who keeps things together, instead of many separate conversations. And partners we know and whose work we stand behind.', + ], + 'points' => [ + [ + 'icon' => 'pencil-square', + 'title' => 'Measuring & planning', + 'description' => 'We come to you, measure up and plan together with you.', + ], + [ + 'icon' => 'wrench-screwdriver', + 'title' => 'Installation by specialists', + 'description' => 'Craftspeople from the region whom we know personally.', + ], + [ + 'icon' => 'user', + 'title' => 'One point of contact', + 'description' => 'Coordinated from a single source, from first idea to handover.', + ], + ], + ], + 'marken_growth' => [ + 'enabled' => true, + 'title' => 'Our brand world is growing', + 'text' => 'We continuously extend our portfolio with manufacturers that match our standards. Missing a particular brand or have a specific wish? Talk to us – our network often turns up exactly the right thing.', + ], + 'marken_cta' => [ + 'title' => 'Get to know our brands', + 'text' => 'Ideally where you can touch them: in our showrooms. Arrange a no-obligation consultation – we take our time.', + 'button_text' => 'Request an appointment', + 'button_link' => '/contact', + 'whatsapp_text' => 'Message us on WhatsApp', + ], 'cta_section' => [ 'title' => 'Your Next Step', - 'subtitle' => 'Whether real estate investment, supply chain partnership, or interior network – speak directly with us.', - 'button_text' => 'Get in Touch', + 'subtitle' => 'Whether a single piece of furniture, a complete interior or a project fit-out – talk to us. Personal, straightforward, reliable.', + 'button_text' => 'Request an appointment', 'button_link' => '/contact', ], + 'cta_section_immobilien' => [ + 'title' => 'Your next step', + 'subtitle' => 'Exposé, purchase process or furnishing after the purchase – talk to us. Personal, straightforward, reliable.', + 'button_text' => 'Request a conversation', + 'button_link' => '/immobilien/kontakt', + ], 'immobilien_hero' => [ 'title' => 'Invest in the Future – Dubai, Lisbon & More.', 'subtitle' => 'Exclusive off-market projects and high-yield investments. Personally curated and guided by Marcel Scheibe.', @@ -571,65 +831,197 @@ return [ ], ], ], - 'faq' => [ - 'title' => 'Frequently Asked Questions', - 'subtitle' => 'Here you will find answers to the most common questions about B2in and our ecosystem.', + 'magazin_list_immobilien' => [ + 'title' => 'B2in Real Estate Magazine', + 'subtitle' => 'Market analyses and practical knowledge about real estate investments in Dubai – purchase process, districts and the role of interiors for the return.', + 'read_more' => 'Read More', + 'load_more' => 'Load More Articles', + ], + 'immobilien_about_hero' => [ + 'title' => 'B2in Real Estate: Our Mission', + 'quote' => '"My mission is to connect two worlds that belong together: international real estate and high-quality interiors. B2in accompanies investors personally through the purchase – and then furnishes the property from a single source.

We build bridges – between European design, international markets, and people\'s homes."', + 'founder_name' => 'Marcel Scheibe', + 'founder_title' => 'Founder & CEO, B2in', + 'image' => 'b2in/about-hero.jpg', + 'image_alt' => 'Marcel Scheibe, Founder and CEO of B2in', + 'card_title' => 'B2in Real Estate', + 'card_text' => 'Connecting Design and Property', + ], + 'immobilien_our_story' => [ + 'title' => 'Our Story', + 'timeline' => [ + [ + 'title' => 'The Idea', + 'description' => 'In 2024, we identified a crucial gap: property buyers are looking for seamless furnishing solutions, and local retailers need visibility. These two worlds belong together.', + 'icon' => 'light-bulb', + ], + [ + 'title' => 'The Foundation', + 'description' => 'We build B2in as a network: regional interior specialists, European manufacturers and real estate professionals – fair, transparent and with a personal point of contact.', + 'icon' => 'rocket-launch', + ], + [ + 'title' => 'The Expansion', + 'description' => '2025/2026 adds international real estate: selected projects in Dubai, reviewed from an investor\'s perspective and accompanied personally. Interiors remain the core – both areas complement each other.', + 'icon' => 'globe-alt', + ], + [ + 'title' => 'The Vision', + 'description' => 'B2in connects design and property – locally rooted, internationally connected. In every region, the reliable partner for real estate investments and exclusive interior concepts.', + 'icon' => 'star', + ], + ], + 'summary' => 'What began as a vision to strengthen regional trade is today a network that connects real estate and interiors. B2in bridges the gap between international investments and local expertise – with Marcel Scheibe as the face and point of contact.', + ], + 'immobilien_about_image_break' => [ + 'image' => 'b2in/best-of-two-worlds.jpg', + 'image_alt' => 'B2in – Connecting Real Estate and Interior', + 'quote' => 'Locally rooted, internationally connected – that is B2in.', + 'author' => 'Marcel Scheibe', + ], + 'immobilien_our_values' => [ + 'title' => 'Our Values', + 'subtitle' => 'These six pillars guide our daily actions and define who we are as a company and what we stand for.', + 'values' => [ + [ + 'title' => 'Personal Guidance', + 'description' => 'From the first conversation through the purchase process to the furnished property: one point of contact who knows the markets from an investor\'s perspective.', + 'icon' => 'light-bulb', + ], + [ + 'title' => 'Connectivity', + 'description' => 'We connect international real estate markets with local expertise and European manufacturers with the people who value their products.', + 'icon' => 'globe-alt', + ], + [ + 'title' => 'Quality', + 'description' => 'Uncompromising standards – in the selection of projects, the review of developers and the curation of interiors.', + 'icon' => 'check-badge', + ], + [ + 'title' => 'Trust', + 'description' => 'Transparent models and reliable partnerships are our foundation. Behind B2in stands a face – and the promise that we deliver what we commit to.', + 'icon' => 'user-group', + ], + [ + 'title' => 'Reliability', + 'description' => 'German contractual certainty meets the dynamics of international markets: clear processes, vetted partners, secured payment routes.', + 'icon' => 'arrow-path', + ], + [ + 'title' => 'Design Excellence', + 'description' => 'Design is the core of our value creation – from international architecture to curated interiors that turn a property into a home.', + 'icon' => 'cube-transparent', + ], + ], + ], + 'contact_form_immobilien' => [ + 'hero' => [ + 'title' => 'Questions about a
project?', + 'subtitle' => 'Exposé, purchase process or furnishing after the purchase – write to us. Marcel Scheibe will get back to you personally.', + ], + ], + 'faq_immobilien' => [ + 'title' => 'Frequently Asked Questions about Real Estate', + 'subtitle' => 'Answers about investments in Dubai, the purchase process and how B2in accompanies you.', 'questions' => [ - [ - 'question' => 'What is B2in and what services do you offer?', - 'answer' => 'B2in is the bridge between international premium real estate and exclusive interior concepts. We offer real estate investors access to off-market projects (focus: Dubai) and guide them through the entire purchase process. At the same time, we provide developers with rigorous supply chain management for sourcing German quality furniture and are building an innovative interior network for local retailers.', - ], - [ - 'question' => 'What does supply chain management mean at B2in?', - 'answer' => 'For international property developers, we act as an extended arm in Germany. We take over the operational and strategic management of furniture and interior procurement. This means: We secure supply contracts, monitor milestones directly with manufacturers, and escalate immediately to management level in case of deviations. Our goal is absolute contractual reliability and on-time delivery without friction losses.', - ], - [ - 'question' => 'How can I become a partner at B2in?', - 'answer' => 'Our network is aimed at property developers, brokers, and regional furniture retailers. If you are a developer seeking German procurement reliability or a retailer wanting to become part of our future "Local for Local" network to receive qualified leads from property buyers, simply use our contact form. We evaluate every partnership individually for quality and fit.', - ], [ 'question' => 'How does real estate investment through B2in work?', - 'answer' => 'We don\'t just broker, we guide you. Marcel Scheibe is your personal advisor who knows the market (especially Dubai) from his own investor perspective. After a needs analysis, we present you with exclusive projects (e.g., from Azizi Developments). The purchase process itself is maximally secured through Dubai\'s government escrow system. Your special advantage: As a B2in client, you subsequently receive exclusive access to our network to furnish your investment turnkey and optimized for returns.', + 'answer' => 'We do not just broker, we accompany you. Marcel Scheibe is your personal advisor who knows the Dubai market from his own investor perspective. After a needs analysis, we present selected projects, for example by Azizi Developments. The purchase process itself is secured by the government escrow system in Dubai. Your advantage as a B2in client: afterwards we furnish your property turnkey and with the return in mind.', ], [ 'question' => 'What makes B2in a trustworthy partner?', - 'answer' => 'Trust comes from transparency and personal market experience. B2in founder Marcel Scheibe invests himself in the markets we offer and is regularly on-site to check construction progress. We combine the immense dynamics of international markets (like Dubai) with German reliability, strict contract management, and a curated network of premium partners like CABINET. With us, you always have a personal contact who represents your interests.', + 'answer' => 'Trust comes from transparency and first-hand market experience. B2in founder Marcel Scheibe invests in the markets we offer himself and is regularly on site to check construction progress. We combine the dynamics of international markets with German reliability and a personal contact who represents your interests.', ], ], 'sections' => [ [ - 'title' => 'Focus: Real Estate Investors (B2C)', + 'title' => 'For Real Estate Investors', 'icon' => 'home-modern', 'questions' => [ [ 'question' => 'Do I need to travel to Dubai personally for the property purchase?', - 'answer' => 'No, the entire purchase process can be completed fully digitally and legally binding from abroad. The Dubai Land Department offers highly secure processes for this. While we do recommend our investors to visit and experience the city\'s dynamics firsthand, your physical presence is not strictly required for the legally secure acquisition.', + 'answer' => 'No, the entire purchase process can be completed digitally and legally binding from a distance. The Dubai Land Department provides secure processes for this. We do recommend that investors get a picture of the city on site – but your presence is not required for a legally secure purchase.', ], [ 'question' => 'What happens after the property purchase? Do you help with furnishing?', - 'answer' => 'This is exactly where the great advantage of the B2in ecosystem lies. Our service doesn\'t end with the purchase contract. As a B2in client, you receive exclusive access to our interior network. We support you in furnishing your property turnkey and optimized for returns with quality furniture – the perfect foundation for lucrative short-term rentals.', + 'answer' => 'This is exactly where the B2in advantage lies. Our service does not end with the purchase contract. We help you furnish your property turnkey and with the return in mind using quality furniture – the basis for a successful rental.', ], [ 'question' => 'What tax advantages does an investment in Dubai offer?', - 'answer' => 'Dubai offers a globally unique economic environment. There is neither income tax on rental income nor capital gains tax on the sale of the property. Combined with the strong rental yields, this makes the location strategically valuable for international investors.', + 'answer' => 'Dubai offers a special economic environment: there is neither income tax on rental income nor capital gains tax on the sale of the property. Combined with the rental yields, this makes the location attractive for international investors. Please have your personal tax situation checked by a tax advisor.', + ], + ], + ], + ], + ], + 'faq' => [ + 'title' => 'Frequently Asked Questions', + 'subtitle' => 'Answers to the questions we are asked most often – about advice, bespoke furniture and fitting out projects.', + 'questions' => [ + [ + 'question' => 'What is B2in Interior and what do you offer?', + 'answer' => 'B2in Interior stands for high-quality interiors from strong brands – CABINET, Rotpunkt Küchen and Febrü – complemented by a network of regional specialists. We support private customers from a single bespoke piece to a complete interior, and fit out properties, offices and developments in full. In addition, we accompany selected international real estate projects.', + ], + [ + 'question' => 'What does fitting out a project involve?', + 'answer' => 'For properties, offices and developments we take on the complete fit-out, coordinated from a single source. We manage the procurement of furniture and interiors both operationally and strategically: we secure supply contracts, monitor milestones directly with the manufacturers and escalate deviations immediately. The goal is contractual reliability and on-time delivery without friction.', + ], + [ + 'question' => 'How can I become a partner?', + 'answer' => 'Our network covers specialist firms and craftspeople around interiors – from installation to adjacent trades. If you are interested, write to us using the contact form. We review every collaboration individually, because we stand behind the work of our partners.', + ], + [ + 'question' => 'How does a consultation work?', + 'answer' => 'It starts with a conversation – in the showroom, at your home or by phone. We look at the rooms involved, how you use them and what matters to you. Measuring up on site follows, then the planning. You see your solution before anything is made. Appointments can be arranged through the form on this page or by phone.', + ], + [ + 'question' => 'What makes B2in Interior a reliable partner?', + 'answer' => 'We work with a manageable number of selected manufacturers instead of arbitrary catalogues – and we stand behind their work. Behind B2in Interior there is a face and a fixed point of contact in Marcel Scheibe, from the first conversation to after installation. For the trades around the interior we work with specialist firms from the region that we know.', + ], + ], + 'sections' => [ + [ + 'title' => 'For your home', + 'icon' => 'home-modern', + 'questions' => [ + [ + 'question' => 'Do I have to furnish the whole flat at once?', + 'answer' => 'No. Many projects start with a single piece – a fitted wardrobe under a sloping ceiling, a new kitchen, a workspace. Whether more follows is up to you. We plan in a way that lets a later addition fit in rather than look bolted on.', + ], + [ + 'question' => 'What does bespoke cost compared to off-the-shelf furniture?', + 'answer' => 'That depends on the room, the materials and the fittings – a serious figure is only possible after measuring up. What can be said: bespoke uses the space standard furniture leaves behind – slopes, niches, unusual heights. You pay for the space you would otherwise give away. You receive the quote before anything is made, binding and itemised.', + ], + [ + 'question' => 'Do you handle installation as well?', + 'answer' => 'Yes. Delivery and installation run through us or through specialist firms from our network that we work with regularly. You have one point of contact who coordinates the appointments – including when several trades are involved.', + ], + [ + 'question' => 'Can I see the brands beforehand?', + 'answer' => 'Yes, in the showroom in Bielefeld you can take your time with surfaces, fronts and fittings. Photos and samples help with a pre-selection, but they do not replace the direct impression. It is best to arrange an appointment so we can take the time.', ], ], ], [ - 'title' => 'Focus: B2B Partners (Brokers & Retailers)', + 'title' => 'For projects and properties', 'icon' => 'briefcase', 'questions' => [ [ - 'question' => 'How do real estate brokers benefit from a partnership with B2in?', - 'answer' => 'B2in offers brokers an exclusive tool for client retention: You can present your property buyers with access to our closed interior network as an exclusive "closing gift." At the same time, as a broker, you benefit from our technical clearing system with a passive lifetime compensation on your clients\' furniture purchases.', + 'question' => 'From what size is a project fit-out worthwhile?', + 'answer' => 'There is no fixed lower limit. Working together makes sense as soon as several units or several trades are involved and coordination becomes the actual effort – for office space, furnished residential units or fitting out an entire property.', ], [ - 'question' => 'What exactly is the "Local for Local" marketplace?', - 'answer' => 'Our "Local for Local" principle is the counter-concept to anonymous online furniture giants. We make the immediately available stock and showroom pieces of regional specialist retailers (the "Local Express" pillar) transparent to a closed customer base. This strengthens local trade on-site while offering buyers insider rates for premium furnishing.', + 'question' => 'How do you ensure delivery dates are met?', + 'answer' => 'We work with binding supply contracts, track milestones directly with the manufacturer and report deviations as soon as they become visible – not just on the delivery date. Where several trades are involved, we handle the coordination between them so the sequence on site works.', ], [ - 'question' => 'Is the interior network publicly accessible to everyone?', - 'answer' => 'No. To protect the exclusive insider rates and discounts from our manufacturers and local retailers, the B2in furniture marketplace operates as a "closed shop." Access is exclusively through invitations – for example, through our partner brokers upon property acquisition.', + 'question' => 'Do you work with architects and planning offices?', + 'answer' => 'Yes. We take on existing plans and implement them, or come in earlier when furniture and floor plan still need to be matched up. For special solutions we talk directly to the manufacturers.', + ], + [ + 'question' => 'How do I join the regional network?', + 'answer' => 'Our network covers specialist firms and craftspeople around interiors – from installation to adjacent trades. If you are interested, write to us using the contact form. We review every collaboration individually, because we stand behind the work of our partners.', ], ], ], @@ -650,14 +1042,35 @@ return [ 'subject' => 'Subject *', 'message' => 'Your Message *', ], - 'subjects' => [ - '' => 'Select a subject', - 'immobilien' => 'International Real Estate', - 'supply_chain' => 'Supply Chain Management', - 'general' => 'General Inquiry', - 'partnership' => 'Partnership', - 'press' => 'Press', - 'career' => 'Career', + 'subject_placeholder' => 'Select a subject', + 'subject_groups' => [ + 'interior' => [ + 'label' => 'Interior', + 'subjects' => [ + 'beratung' => 'Consultation appointment', + 'massanfertigung' => 'Bespoke furniture for my home', + 'kueche' => 'Kitchen planning', + 'supply_chain' => 'Fitting out a project', + ], + ], + 'immobilien' => [ + 'label' => 'Real Estate', + 'subjects' => [ + 'immobilien' => 'Investment consultation', + 'expose' => 'Exposé and availability', + 'kaufprozess' => 'Questions about the purchase process', + 'einrichtung' => 'Furnishing after the purchase', + ], + ], + 'b2in' => [ + 'label' => 'B2in', + 'subjects' => [ + 'partnership' => 'Working together in the network', + 'general' => 'General Inquiry', + 'press' => 'Press', + 'career' => 'Career', + ], + ], ], 'placeholders' => [ 'message' => 'Your message...', @@ -704,14 +1117,14 @@ return [ ], 'about_hero' => [ 'title' => 'About B2in: Our Mission', - 'quote' => '"My mission is to connect two worlds that belong together: international real estate and exclusive interior design. B2in gives the local specialist the digital tools and the property developer the operational partner on the ground.

We build bridges – between European design, international markets, and people\'s homes."', + 'quote' => '"Good interiors are not created in a catalogue, but in conversation. That is why we work with a few selected manufacturers we know – and with specialist firms from the region whose work we stand behind.

My standard is simple: one point of contact, from the first measurement to after installation."', 'founder_name' => 'Marcel Scheibe', 'founder_title' => 'Founder & CEO, B2in', 'image' => 'b2in/about-hero.jpg', 'image_alt' => 'Marcel Scheibe, Founder and CEO of B2in', 'card_title' => 'B2in', - 'card_text' => 'Connecting Design and Property', + 'card_text' => 'Premium interiors. Made personal.', ], 'broker_section' => [ @@ -988,7 +1401,7 @@ return [ ], 'magazin_list' => [ 'title' => 'B2in Magazine', - 'subtitle' => 'Insights, market analyses, and practical knowledge about real estate investments in Dubai, interior concepts, and the B2in ecosystem.', + 'subtitle' => 'Practical knowledge about interiors: custom-made furniture, kitchen planning, brands and the regional furniture trade.', 'read_more' => 'Read More', 'load_more' => 'Load More Articles', ], @@ -997,26 +1410,26 @@ return [ 'timeline' => [ [ 'title' => 'The Idea', - 'description' => 'In 2024, we identified a crucial gap: Local retailers need digital visibility, and property buyers are looking for seamless furnishing solutions. These two worlds belong together.', + 'description' => 'In 2024 the starting point was a simple observation: anyone wanting a high-quality interior finds plenty of catalogues online – but rarely someone who listens, measures up and stands behind the result.', 'icon' => 'light-bulb', ], [ 'title' => 'The Foundation', - 'description' => 'We are developing the B2in platform: an ecosystem that connects local interior specialists, European manufacturers, and real estate professionals on a shared platform – fair, transparent, and technology-driven.', + 'description' => 'We are building the network: selected manufacturers, specialist firms and craftspeople from the region – connected by the same conviction that quality and keeping your word belong together.', 'icon' => 'rocket-launch', ], [ 'title' => 'The Expansion', - 'description' => '2025/2026 sees B2in expanding its ecosystem: International real estate and supply chain management for developers become the dominant pillar. The local furniture marketplace remains as a strong complementary area.', + 'description' => 'In 2026 interiors move to the centre: CABINET, Rotpunkt Küchen and Febrü form the core, complemented by a network of regional specialists. International real estate remains as a complementary area.', 'icon' => 'globe-alt', ], [ 'title' => 'The Vision', - 'description' => 'B2in becomes the central network for "Design & Property" – locally rooted, internationally connected. In every region, the reliable partner for real estate investments and exclusive interior concepts.', + 'description' => 'B2in Interior becomes the first address in the region for high-quality interiors – from a single fitted wardrobe to a complete fit-out. Locally rooted, internationally connected.', 'icon' => 'star', ], ], - 'summary' => 'What began as a vision to strengthen local trade is today a network that seamlessly connects real estate and interior. B2in bridges the gap between international investments and local expertise – with Marcel Scheibe as the face and point of contact.', + 'summary' => 'What began as an idea – to make high-quality interiors personal again – is today a network of strong brands and regional specialists, with Marcel Scheibe as the face and fixed point of contact.', ], 'about_image_break' => [ 'image' => 'b2in/best-of-two-worlds.jpg', @@ -1026,37 +1439,37 @@ return [ ], 'our_values' => [ 'title' => 'Our Values', - 'subtitle' => 'These six pillars guide our daily actions and define who we are as a company and what we stand for.', + 'subtitle' => 'Six principles we want to be measured by – in the consultation, in the planning and after installation.', 'values' => [ [ - 'title' => 'Innovation', - 'description' => 'We develop digital solutions that give real estate professionals and local interior specialists a genuine competitive advantage.', - 'icon' => 'light-bulb', + 'title' => 'Advice, not sales', + 'description' => 'We first ask how you use your rooms and only then suggest something. Sometimes the right answer is a smaller piece than expected.', + 'icon' => 'chat-bubble-left-right', ], [ - 'title' => 'Connectivity', - 'description' => 'We connect international real estate markets with local expertise and European manufacturers with the people who value their products.', - 'icon' => 'globe-alt', + 'title' => 'Bespoke work', + 'description' => 'Slopes, niches and unusual heights are not a problem but the reason for bespoke furniture. We plan the space that standard furniture leaves behind.', + 'icon' => 'cube-transparent', ], [ - 'title' => 'Quality', - 'description' => 'Uncompromising standards – in the selection of our partners, the curation of interiors, the monitoring of supply chains, and the technology that holds it all together.', + 'title' => 'Selected brands', + 'description' => 'A few manufacturers we know, rather than an arbitrary range. We stand behind the brands we carry.', 'icon' => 'check-badge', ], [ - 'title' => 'Trust', - 'description' => 'Transparent models and reliable partnerships are our foundation. Behind B2in stands a face – and the promise that we deliver what we commit to.', + 'title' => 'Regionally rooted', + 'description' => 'We work with specialist firms and craftspeople from the region – people we know and who are on site when it matters.', + 'icon' => 'map-pin', + ], + [ + 'title' => 'One point of contact', + 'description' => 'From the first conversation through measuring up to after installation, the contact stays the same – including when several trades are involved.', 'icon' => 'user-group', ], [ - 'title' => 'Sustainability', - 'description' => 'We strengthen local trade, optimize transport routes, and ensure that international procurement runs responsibly and efficiently.', - 'icon' => 'arrow-path', - ], - [ - 'title' => 'Design Excellence', - 'description' => 'Design is the core of our value creation – from international architecture to curated interiors to the intuitive design of our digital platform.', - 'icon' => 'cube-transparent', + 'title' => 'Reliability', + 'description' => 'We keep our commitments on dates and prices. If something shifts, we say so as soon as we know – not on the delivery date.', + 'icon' => 'shield-check', ], ], ], @@ -1474,12 +1887,77 @@ return [ ], ], 'articles' => [ + 6 => [ + 'id' => 6, + 'slug' => 'massanfertigung-warum-der-millimeter-zaehlt', + 'title' => 'Bespoke, not off the shelf:
Why the millimetre decides what a room can do', + 'subtitle' => 'Sloping ceilings, niches, unusual heights: where standard furniture gives up, bespoke work begins – turning dead space into usable area.', + 'image' => 'b2in/stileigentum.jpg', + 'category' => 'Interiors', + 'date' => 'August 26, 2026', + 'readTime' => '5 min read', + 'author' => [ + 'name' => 'Marcel Scheibe', + 'bio' => 'Founder & CEO of B2in Interior. With roots in the furniture trade, he accompanies customers from planning to after installation.', + 'avatar' => 'b2in/marcel-scheibe.jpg', + ], + 'content' => [ + 'intro' => 'Almost every home has one: the corner where nothing quite fits. The slope under the roof, the niche beside the chimney, the hallway too narrow for a wardrobe and too wide for nothing. Put a standard piece there and you lose twice – a gap at the front, wasted air at the back. Bespoke work starts exactly there, and the difference is not made by the furniture but long before: when measuring up.', + 'sections' => [ + [ + 'title' => 'Measuring up is not a formality', + 'content' => 'Walls are rarely straight, floors rarely level, and in older buildings the height can vary by several centimetres across a single room. Measuring on site records these deviations instead of ignoring them. Only then can you plan something that fits to the millimetre – without panels to hide things and without joints where dust collects.', + ], + [ + 'title' => 'The space you would otherwise give away', + 'content' => 'With a sloping ceiling the gain is not in the look but in the area. A wardrobe that follows the slope uses the part a straight carcass cuts off – depending on the angle, a substantial share of the storage. The same applies to niches: a fitted unit gains depth a free-standing piece can never reach, because it needs clearance from the wall.', + ], + [ + 'title' => 'Where bespoke is not worth it', + 'content' => 'Being honest means naming the other side too: with straight walls, usual ceiling heights and standard dimensions, a good series product is often the more sensible choice – available faster and cheaper. So the question is not "bespoke or not" but: where in this room is there actually something to gain? That answer belongs at the start of the consultation, not at the end.', + ], + ], + ], + ], + 7 => [ + 'id' => 7, + 'slug' => 'kueche-planen-vom-wunsch-zum-grundriss', + 'title' => 'Planning a kitchen:
How wishes turn into a floor plan', + 'subtitle' => 'Before anyone talks about fronts and colours, a kitchen is decided by three questions – and at first they have little to do with design.', + 'image' => 'b2in/style2own.jpg', + 'category' => 'Kitchen', + 'date' => 'August 20, 2026', + 'readTime' => '6 min read', + 'author' => [ + 'name' => 'Marcel Scheibe', + 'bio' => 'Founder & CEO of B2in Interior. With roots in the furniture trade, he accompanies customers from planning to after installation.', + 'avatar' => 'b2in/marcel-scheibe.jpg', + ], + 'content' => [ + 'intro' => 'Most kitchen conversations start with the front: matt or gloss, light or dark, handle or handleless. Understandable, because that is what you see. Only nothing is decided at that point. Whether a kitchen works day to day depends on questions that can be settled before the first colour sample – and that are hard to correct later.', + 'sections' => [ + [ + 'title' => 'Question one: how do you actually cook?', + 'content' => 'Not how you would like to cook – how you really do. Anyone cooking fresh every day needs worktop beside the hob and short distances between fridge, sink and cooking zone. Anyone using the kitchen mainly as a meeting point plans differently: more room to stand, a surface where someone can sit while food is being made. Both are good kitchens, but they are not the same kitchen.', + ], + [ + 'title' => 'Question two: what does the room dictate?', + 'content' => 'Windows, doors, connections and load-bearing walls set the frame before anyone plans anything. Some of it can be moved, some cannot – and what that costs should be on the table before the planning builds on it. Ceiling height belongs here too: it decides whether wall units can run all the way up or whether a dust ledge is left above them.', + ], + [ + 'title' => 'Question three: what should still work in ten years?', + 'content' => 'A kitchen stays for a long time. In many systems fronts can be replaced later; carcass dimensions and the layout practically cannot. So it pays to focus on durability for drawers, fittings and the worktop, and to keep the appetite for fashion to the parts that can be swapped. The colour comes after that – and then it may well be bold.', + ], + ], + ], + ], 1 => [ 'id' => 1, 'title' => 'Security Over Bureaucracy:
Why Dubai\'s Escrow System Surprises German Investors', 'subtitle' => 'How the government-regulated trust system in Dubai consistently protects investor funds – and why it is far ahead of international standards.', 'image' => 'b2in/magazin-1.jpg', 'category' => 'Dubai Investment', + 'world' => 'immobilien', 'date' => 'March 10, 2026', 'readTime' => '6 min read', 'author' => [ @@ -1511,6 +1989,7 @@ return [ 'subtitle' => 'Strategic waterfront location, high rental demand, and enormous potential for capital appreciation – Al Jaddaf is the hidden champion among Dubai\'s districts.', 'image' => 'b2in/magazin-2.jpg', 'category' => 'Dubai Investment', + 'world' => 'immobilien', 'date' => 'March 5, 2026', 'readTime' => '7 min read', 'author' => [ @@ -1542,6 +2021,7 @@ return [ 'subtitle' => 'Why turnkey interior concepts make the difference between average and premium returns.', 'image' => 'b2in/magazin-3.jpg', 'category' => 'Returns & Interior', + 'world' => 'immobilien', 'date' => 'February 25, 2026', 'readTime' => '6 min read', 'author' => [ diff --git a/resources/lang/en/ui.php b/resources/lang/en/ui.php index 772c48d3..0073bc20 100644 --- a/resources/lang/en/ui.php +++ b/resources/lang/en/ui.php @@ -16,9 +16,89 @@ return [ 'menu_close' => 'Close menu', 'menu_open' => 'Open menu', + // World switcher (Interior / Real estate) + // Farb-Schalter für die Abstimmung der Palette (Nachtrag 18) + 'palette' => [ + 'label' => 'Colour scheme', + 'classic' => 'Current', + 'modern' => 'New', + 'draft' => 'Draft', + ], + // Slideshow in the draft hero (Nachtrag 19) + 'hero_slider' => [ + 'label' => 'Glimpses of our brands', + 'dots' => 'Choose image', + 'dot' => 'Image :n of :total: :label', + 'empty' => 'No images yet.', + ], + 'worlds_label' => 'Switch area', + 'world_interior' => 'Interior', + 'world_immobilien' => 'Real estate', + + 'display_feed_title' => 'Live view of the showroom display', + 'references_empty' => 'The first projects will appear here as soon as we are allowed to show them.', + + // Appointment widget + 'appointment' => [ + 'name' => 'Name', + 'email' => 'Email', + 'phone' => 'Phone', + 'topic' => 'Subject', + 'topic_placeholder' => 'Please choose', + 'preferred_date' => 'Preferred date', + 'message' => 'Message', + 'message_placeholder' => 'What is it about? Two sentences are enough.', + 'submit' => 'Send request', + 'sending' => 'Sending …', + 'success' => 'Thank you! We will get back to you shortly to arrange the appointment.', + 'new_request' => 'Another request', + 'date_in_past' => 'Please choose a date from today onwards.', + 'whatsapp_intro' => 'Fastest via WhatsApp – we prepare the message with your details.', + 'or_form' => 'or use the form', + 'whatsapp_text' => 'Hello B2in Interior, I would like to arrange an appointment.', + 'whatsapp_note' => 'Opens WhatsApp with a prepared text. You send the message yourself.', + ], + + // Kernmarken + 'all_brands_cta' => 'View all brands', + 'brands' => 'Brands', + 'whatsapp' => 'Message us on WhatsApp', + 'all_brands' => 'All brands', + 'brand_services' => 'Services and specialities', + 'brand_cta_title' => 'Interested in :name?', + 'brand_cta_text' => 'Arrange an appointment – we advise you personally and without obligation.', + 'request_appointment' => 'Request an appointment', + 'brand_website' => 'Visit the manufacturer', + 'other_brands' => 'Our other core brands', + 'more_about' => 'More about :name', + + // Brand gallery – realised projects vs. drafts + 'brand_gallery_title' => 'Inspiration and drafts', + 'brand_gallery_intro' => 'Visualisations that show what is possible in a room – before the first piece of furniture is in place.', + 'brand_gallery_title_inspirations' => 'Inspiration and drafts', + 'brand_gallery_intro_inspirations' => 'Visualisations that show what is possible in a room – before the first piece of furniture is in place.', + 'brand_gallery_title_projects' => 'References', + 'brand_gallery_intro_projects' => 'Completed projects – this is what it looks like once planning and craft are done.', + 'brand_gallery_title_projects_company' => 'References and insights', + 'brand_gallery_intro_projects_company' => 'Completed projects and a look into the workshop where they are made.', + 'brand_gallery_title_mixed' => 'Projects and drafts', + 'brand_gallery_intro_mixed' => 'Completed projects and visualisations that show what is possible in a room.', + 'brand_gallery_tab_projects' => 'Completed projects', + 'brand_gallery_tab_inspirations' => 'Inspiration and drafts', + 'brand_gallery_badge_project' => 'Completed project', + 'brand_gallery_badge_inspiration' => 'Visualisation', + 'brand_gallery_badge_company' => 'From the workshop', + 'brand_gallery_inspiration_note' => 'Drafts and visualisations. They show design possibilities before the first piece of furniture is in place.', + 'previous' => 'Previous image', + 'next' => 'Next image', + // Footer 'founder_ceo' => 'Founder & CEO', 'legal_notice' => 'Legal Notice', + 'privacy_policy' => 'Privacy Policy', + 'terms_of_service' => 'Terms of Service', + 'cookie_policy' => 'Cookie Policy', + 'faq' => 'FAQ', 'cookie_settings' => 'Cookie Settings', // Announcement Bar @@ -34,6 +114,7 @@ return [ // Contact Form 'contact_form' => [ + 'subject_placeholder' => 'Select a subject', 'success_title' => 'Thank you!', 'success_message' => 'Your message has been received. We will get back to you as soon as possible.', 'first_name' => 'First Name *', diff --git a/resources/views/components/appointment-link.blade.php b/resources/views/components/appointment-link.blade.php new file mode 100644 index 00000000..6e5f1f06 --- /dev/null +++ b/resources/views/components/appointment-link.blade.php @@ -0,0 +1,17 @@ +{{-- Link zu einer Terminanfrage. + + Zeigt der Link auf eine Kontaktseite (/contact oder /immobilien/kontakt), oeffnet ein Klick + das schwebende Terminanfrage-Widget statt die Seite zu wechseln – `topic` belegt dabei das + Anliegen vor (z. B. der Marken-Slug auf einer Markenseite). Ohne JavaScript bleibt es ein + normaler Link. Zeigt der Link woandershin (vom CMS geaendert), ist es ein gewoehnlicher Link. + Das Abfangen steckt in resources/js/app.js, das Oeffnen im Widget selbst. --}} +@props(['href', 'topic' => null]) + +@php + $path = (string) (parse_url((string) $href, PHP_URL_PATH) ?? ''); + $opensWidget = in_array(rtrim($path, '/'), ['/contact', '/immobilien/kontakt'], true); +@endphp + +{{ $slot }} diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php index e7c44b9d..ac1f3c3d 100644 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ b/resources/views/components/layouts/app/sidebar.blade.php @@ -161,7 +161,7 @@ {{ __('Übersicht') }} @@ -169,8 +169,12 @@ {{ __('Inhalte') }} - {{ __('Projekte') }} + {{-- Immobilien-Projekte (cms.projects.index) sind nicht freigegeben und bewusst nicht verlinkt (Nachtrag 13). --}} + {{ __('Referenzen') }} + + {{ __('Marken') }} {{ __('Magazin') }} diff --git a/resources/views/components/palette-switch.blade.php b/resources/views/components/palette-switch.blade.php new file mode 100644 index 00000000..376b1e75 --- /dev/null +++ b/resources/views/components/palette-switch.blade.php @@ -0,0 +1,55 @@ +{{-- Farb-Schalter für die Abstimmung (Nachtrag 18 + 19): bisherige Creme-Palette, die hellere + Variante „Neu“ und der Entwurf vom 24.09. Schwimmt am linken Rand in der Mitte – unten links + sitzt der Cookie-Button, unten rechts die Terminanfrage. + „Bisher“ und „Neu“ schalten im Browser um (localStorage b2in-palette, im gesetzt, + ?palette=modern|classic per Link). „Entwurf“ ist eine eigene Seite (/entwurf) mit fester + Palette; von dort führen die beiden anderen Einträge zurück auf die Startseite. --}} +@props(['draft' => false]) + +
+
+ + + @if ($draft) + + {{ __('ui.palette.classic') }} + + + {{ __('ui.palette.modern') }} + + + {{ __('ui.palette.draft') }} + + @else + + + + {{ __('ui.palette.draft') }} + + @endif +
+
diff --git a/resources/views/components/web/hero-flank-left.blade.php b/resources/views/components/web/hero-flank-left.blade.php new file mode 100644 index 00000000..72f2e0f1 --- /dev/null +++ b/resources/views/components/web/hero-flank-left.blade.php @@ -0,0 +1,18 @@ +{{-- Linke Flanke des Entwurfs-Heros (Nachtrag 19): schmaler Haken unten links, oben gerundet, + mit Goldlinie. Höhe rund 46 % des Rahmens, Breite aus dem viewBox-Verhältnis. --}} + diff --git a/resources/views/components/web/hero-flank-right.blade.php b/resources/views/components/web/hero-flank-right.blade.php new file mode 100644 index 00000000..0207e11a --- /dev/null +++ b/resources/views/components/web/hero-flank-right.blade.php @@ -0,0 +1,24 @@ +{{-- Rechte Flanke des Entwurfs-Heros (Nachtrag 19): tannengrüner Block mit gerundeter Innenkante, + Goldlinie mit Verlauf entlang der Rundung und einer kurzen Zierlinie wie in der Visualisierung. + Skaliert über die Rahmenhöhe (height: 100 %, Breite aus dem viewBox-Verhältnis). Farben + kommen aus den Tokens, damit die Flanke jede Palette mitmacht. Kann später 1:1 gegen eine + gelieferte SVG-Datei getauscht werden. --}} + diff --git a/resources/views/livewire/admin/cms/articles-index.blade.php b/resources/views/livewire/admin/cms/articles-index.blade.php index ab2e74c9..9b908b22 100644 --- a/resources/views/livewire/admin/cms/articles-index.blade.php +++ b/resources/views/livewire/admin/cms/articles-index.blade.php @@ -17,6 +17,7 @@ state([ 'subtitle' => '', 'image' => '', 'category' => '', + 'world' => 'interior', 'date_label' => '', 'read_time' => '', 'authorName' => '', @@ -48,9 +49,10 @@ $articles = computed( $openCreate = function () { $this->editingId = null; - $this->reset(['slug', 'articleTitle', 'subtitle', 'image', 'category', 'date_label', 'read_time', 'authorName', 'authorBio', 'authorAvatar', 'intro', 'sections']); + $this->reset(['slug', 'articleTitle', 'subtitle', 'image', 'category', 'world', 'date_label', 'read_time', 'authorName', 'authorBio', 'authorAvatar', 'intro', 'sections']); $this->is_published = true; $this->order = 0; + $this->world = 'interior'; $this->sections = [['title' => '', 'content' => '']]; $this->showForm = true; }; @@ -65,6 +67,7 @@ $openEdit = function (int $id) { $this->slug = $article->slug; $this->image = $article->image ?? ''; $this->category = $article->category ?? ''; + $this->world = $article->world ?? 'interior'; $this->date_label = $article->date_label ?? ''; $this->read_time = $article->read_time ?? ''; $this->is_published = $article->is_published; @@ -114,6 +117,7 @@ $save = function () { 'subtitle' => $this->subtitle, 'image' => $this->image, 'category' => $this->category, + 'world' => $this->world, 'date_label' => $this->date_label, 'read_time' => $this->read_time, 'authorName' => $this->authorName, @@ -126,6 +130,7 @@ $save = function () { 'subtitle' => 'nullable|string|max:1000', 'image' => 'nullable|string|max:500', 'category' => 'nullable|string|max:255', + 'world' => 'required|in:interior,immobilien', 'date_label' => 'nullable|string|max:100', 'read_time' => 'nullable|string|max:50', 'authorName' => 'nullable|string|max:255', @@ -153,6 +158,7 @@ $save = function () { $article->image = $validated['image'] ?? null; $article->category = $validated['category'] ?? null; + $article->world = $validated['world']; $article->date_label = $validated['date_label'] ?? null; $article->read_time = $validated['read_time'] ?? null; $article->author = [ @@ -225,6 +231,12 @@ $cancelForm = function () { + {{-- Welt: entscheidet, in welcher Magazinliste der Beitrag steht (/magazin oder + /immobilien/magazin) und in welcher Farbwelt die Detailseite rendert. --}} + + Interior + Immobilien + @@ -337,6 +349,9 @@ $cancelForm = function () { @if ($article->category) {{ $article->category }} @endif + @if ($article->world === 'immobilien') + Immobilien + @endif @unless ($article->is_published) Entwurf @endunless diff --git a/resources/views/livewire/admin/cms/brands-index.blade.php b/resources/views/livewire/admin/cms/brands-index.blade.php new file mode 100644 index 00000000..2fb98595 --- /dev/null +++ b/resources/views/livewire/admin/cms/brands-index.blade.php @@ -0,0 +1,674 @@ + '', + 'showForm' => false, + 'editingId' => null, + 'editLocale' => 'de', + 'slug' => '', + 'brandName' => '', + 'tagline' => '', + 'teaser' => '', + 'summary' => '', + 'highlights' => [], + 'description' => '', + 'logo' => '', + 'image' => '', + 'services' => [], + 'sections' => [], + 'widgetLabel' => '', + 'ctaTitle' => '', + 'ctaText' => '', + 'gallery' => [], + 'website_url' => '', + 'showroom' => '', + 'originCity' => '', + 'originDistance' => '', + 'originRole' => '', + 'is_published' => true, + 'order' => 0, +]); + +on(['media-selected' => function ($mediaId, $url, $field) { + $media = $mediaId ? CmsMedia::find($mediaId) : null; + + if (! $media) { + return; + } + + if ($field === 'brand_logo') { + $this->logo = $media->filename; + } + + if ($field === 'brand_image') { + $this->image = $media->filename; + } + + if (str_starts_with($field, 'brand_gallery:')) { + $index = (int) substr($field, strlen('brand_gallery:')); + + if (isset($this->gallery[$index])) { + $this->gallery[$index]['image'] = $media->filename; + } + } +}]); + +$brands = computed( + fn () => CmsBrand::query() + ->when($this->search, fn ($q, $s) => $q->where('slug', 'like', "%{$s}%") + ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(name, '$.de')) LIKE ?", ["%{$s}%"])) + ->ordered() + ->get(), +); + +$openCreate = function () { + $this->editingId = null; + $this->reset(['slug', 'brandName', 'tagline', 'teaser', 'summary', 'description', 'logo', 'image', 'website_url', 'showroom', 'originCity', 'originDistance', 'originRole', 'widgetLabel', 'ctaTitle', 'ctaText']); + $this->sections = []; + $this->highlights = ['', '', '']; + $this->services = [['title' => '', 'description' => '']]; + $this->gallery = []; + $this->is_published = true; + $this->order = (CmsBrand::max('order') ?? 0) + 1; + $this->showForm = true; +}; + +$loadLocaleFields = function (CmsBrand $brand, string $locale) { + $this->brandName = $brand->getTranslation('name', $locale) ?? ''; + $this->tagline = $brand->getTranslation('tagline', $locale) ?? ''; + $this->teaser = $brand->getTranslation('teaser', $locale) ?? ''; + $this->summary = $brand->getTranslation('summary', $locale) ?? ''; + $this->description = CmsFluxEditorHtmlTransformer::toEditor((string) ($brand->getTranslation('description', $locale) ?? '')); + $this->widgetLabel = $brand->getTranslation('widget_label', $locale) ?? ''; + $this->ctaTitle = $brand->getTranslation('cta_title', $locale) ?? ''; + $this->ctaText = $brand->getTranslation('cta_text', $locale) ?? ''; + $this->originCity = $brand->getTranslation('origin_city', $locale) ?? ''; + $this->originDistance = $brand->getTranslation('origin_distance', $locale) ?? ''; + $this->originRole = $brand->getTranslation('origin_role', $locale) ?? ''; +}; + +$openEdit = function (int $id) { + $brand = CmsBrand::find($id); + + if (! $brand) { + return; + } + + $this->editingId = $id; + $this->slug = $brand->slug; + $this->logo = $brand->logo ?? ''; + $this->image = $brand->image ?? ''; + $this->website_url = $brand->website_url ?? ''; + $this->showroom = $brand->showroom ?? ''; + $this->is_published = $brand->is_published; + $this->order = $brand->order ?? 0; + $this->highlights = $brand->highlights ?: ['', '', '']; + $this->services = $brand->services ?: [['title' => '', 'description' => '']]; + $this->gallery = CmsBrand::normalizeGallery($brand->gallery); + // Absätze im Formular als Textblock: eine Leerzeile trennt sie. + $this->sections = collect(CmsBrand::normalizeSections($brand->sections)) + ->map(fn (array $s) => [ + 'title' => $s['title'], + 'paragraphs' => implode("\n\n", $s['paragraphs']), + 'claim' => $s['claim'], + 'quote' => $s['quote'], + 'quote_author' => $s['quote_author'], + 'detail_title' => $s['detail']['title'] ?? '', + 'detail_paragraphs' => implode("\n\n", $s['detail']['paragraphs'] ?? []), + ]) + ->all(); + + $this->loadLocaleFields($brand, $this->editLocale); + $this->showForm = true; +}; + +$switchLocale = function (string $locale) { + $this->editLocale = $locale; + + if ($this->editingId) { + $brand = CmsBrand::find($this->editingId); + + if ($brand) { + $this->loadLocaleFields($brand, $locale); + } + } +}; + +$addHighlight = fn () => $this->highlights[] = ''; + +$removeHighlight = function (int $index) { + unset($this->highlights[$index]); + $this->highlights = array_values($this->highlights); +}; + +$addService = fn () => $this->services[] = ['title' => '', 'description' => '']; + +$removeService = function (int $index) { + unset($this->services[$index]); + $this->services = array_values($this->services); +}; + +$addSection = fn () => $this->sections[] = [ + 'title' => '', 'paragraphs' => '', 'claim' => '', + 'quote' => '', 'quote_author' => '', 'detail_title' => '', 'detail_paragraphs' => '', +]; + +$removeSection = function (int $index) { + unset($this->sections[$index]); + $this->sections = array_values($this->sections); +}; + +$moveSection = function (int $index, int $direction) { + $target = $index + $direction; + + if (! isset($this->sections[$index], $this->sections[$target])) { + return; + } + + [$this->sections[$index], $this->sections[$target]] = [$this->sections[$target], $this->sections[$index]]; +}; + +$addGalleryImage = function (string $category = CmsBrand::GALLERY_PROJECT) { + $this->gallery[] = [ + 'image' => '', + 'category' => in_array($category, CmsBrand::GALLERY_CATEGORIES, true) ? $category : CmsBrand::GALLERY_INSPIRATION, + 'caption' => '', + ]; +}; + +$removeGalleryImage = function (int $index) { + unset($this->gallery[$index]); + $this->gallery = array_values($this->gallery); +}; + +$save = function () { + $validated = validator([ + 'slug' => $this->slug, + 'brandName' => $this->brandName, + 'tagline' => $this->tagline, + 'teaser' => $this->teaser, + 'summary' => $this->summary, + 'website_url' => $this->website_url, + 'showroom' => $this->showroom, + 'originCity' => $this->originCity, + 'originDistance' => $this->originDistance, + 'originRole' => $this->originRole, + 'is_published' => $this->is_published, + 'order' => $this->order, + ], [ + 'slug' => 'required|string|max:255|regex:/^[a-z0-9-]+$/', + 'brandName' => 'required|string|max:255', + 'tagline' => 'nullable|string|max:255', + 'teaser' => 'nullable|string|max:2000', + 'summary' => 'nullable|string|max:4000', + 'website_url' => 'nullable|url|max:500', + 'showroom' => 'nullable|string|max:255', + 'originCity' => 'nullable|string|max:255', + 'originDistance' => 'nullable|string|max:255', + 'originRole' => 'nullable|string|max:500', + 'is_published' => 'boolean', + 'order' => 'integer|min:0', + ], [ + 'slug.regex' => 'Der Slug darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten.', + 'website_url.url' => 'Bitte eine vollständige URL angeben (mit https://).', + ])->validate(); + + $duplicate = CmsBrand::query() + ->where('slug', $validated['slug']) + ->when($this->editingId, fn ($q) => $q->whereKeyNot($this->editingId)) + ->exists(); + + if ($duplicate) { + Flux::toast(variant: 'danger', heading: 'Slug vergeben', text: 'Es gibt bereits eine Marke mit diesem Slug.'); + + return; + } + + $brand = $this->editingId + ? CmsBrand::findOrFail($this->editingId) + : CmsBrand::query()->make(); + + $brand->slug = $validated['slug']; + $brand->setTranslation('name', $this->editLocale, $validated['brandName']); + $brand->setTranslation('tagline', $this->editLocale, $validated['tagline'] ?? ''); + $brand->setTranslation('teaser', $this->editLocale, $validated['teaser'] ?? ''); + $brand->setTranslation('summary', $this->editLocale, $validated['summary'] ?? ''); + $brand->setTranslation('description', $this->editLocale, CmsFluxEditorHtmlTransformer::fromEditor((string) $this->description)); + + $brand->logo = $this->logo ?: null; + $brand->image = $this->image ?: null; + $brand->website_url = $validated['website_url'] ?: null; + $brand->showroom = $validated['showroom'] ?: null; + + $brand->highlights = collect($this->highlights) + ->map(fn ($h) => trim((string) $h)) + ->filter() + ->values() + ->toArray(); + + $brand->services = collect($this->services) + ->filter(fn ($s) => ! empty($s['title']) || ! empty($s['description'])) + ->values() + ->toArray(); + + $brand->gallery = CmsBrand::normalizeGallery($this->gallery); + + $brand->setTranslation('widget_label', $this->editLocale, trim((string) $this->widgetLabel)); + $brand->setTranslation('cta_title', $this->editLocale, trim((string) $this->ctaTitle)); + $brand->setTranslation('cta_text', $this->editLocale, trim((string) $this->ctaText)); + + // Herkunfts-Sektion: ohne Fertigungsort erscheint die Marke dort nicht. + $brand->setTranslation('origin_city', $this->editLocale, trim((string) ($validated['originCity'] ?? ''))); + $brand->setTranslation('origin_distance', $this->editLocale, trim((string) ($validated['originDistance'] ?? ''))); + $brand->setTranslation('origin_role', $this->editLocale, trim((string) ($validated['originRole'] ?? ''))); + + // normalizeSections() nimmt die Textblöcke entgegen und trennt sie an den Leerzeilen. + $brand->sections = CmsBrand::normalizeSections(collect($this->sections)->map(fn (array $s) => [ + 'title' => $s['title'] ?? '', + 'paragraphs' => $s['paragraphs'] ?? '', + 'claim' => $s['claim'] ?? '', + 'quote' => $s['quote'] ?? '', + 'quote_author' => $s['quote_author'] ?? '', + 'detail' => [ + 'title' => $s['detail_title'] ?? '', + 'paragraphs' => $s['detail_paragraphs'] ?? '', + ], + ])->all()); + + $brand->is_published = $validated['is_published']; + $brand->order = $validated['order']; + $brand->save(); + + $this->showForm = false; + $this->editingId = null; + + Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Marke wurde erfolgreich gespeichert.'); +}; + +$togglePublished = function (int $id) { + $brand = CmsBrand::findOrFail($id); + $brand->update(['is_published' => ! $brand->is_published]); + + Flux::toast(heading: 'Status geändert', text: $brand->is_published ? 'Veröffentlicht' : 'Entwurf'); +}; + +$deleteBrand = function (int $id) { + CmsBrand::findOrFail($id)->delete(); + + Flux::toast(variant: 'success', heading: 'Gelöscht', text: 'Marke wurde entfernt.'); +}; + +$cancelForm = function () { + $this->showForm = false; + $this->editingId = null; +}; + +?> + +
+
+ Kernmarken +
+ @foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label) + {{ strtoupper($code) }} + @endforeach + Neue Marke +
+
+ + +
+ +
+ Die Marken erscheinen als Kacheln auf der Startseite unter „Unsere Kernmarken" und + bekommen jeweils eine eigene Detailseite unter /marken/<slug>. + Die Reihenfolge steuert das Feld „Sortierung". +
+
+
+ +
+ +
+ + @if ($showForm) + +
+ {{ $editingId ? 'Marke bearbeiten' : 'Neue Marke' }} +
+ @foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label) + {{ strtoupper($code) }} + @endforeach +
+
+ +
+ + @error('slug') {{ $message }} @enderror + + + @error('brandName') {{ $message }} @enderror + +
+ +
+ + + + @error('website_url') {{ $message }} @enderror + + +
+ +
+ + + +
+ + {{-- Fertigungsort für „Aus der Region gefertigt“ (Startseite und /marken) --}} +
+ Fertigungsort + + Erscheint in der Sektion „Aus der Region gefertigt“ auf der Startseite und unter /marken. + Ohne Ort bleibt die Marke dort weg. Die Position in der Karte ist fest hinterlegt. + +
+ + +
+ +
+
+
+ + {{-- Drei Merkmale für die Übersichtsseite --}} +
+
+ Merkmale auf /marken + Merkmal hinzufügen +
+
+ @foreach ($highlights as $i => $highlight) +
+ + +
+ @endforeach +
+
+ + {{-- Bilder --}} +
+
+ +
+ @if ($logo) +
+ +
+ @endif +
+ +

{{ $logo ?: 'Kein Logo' }}

+
+
+
+ +
+ +
+ @if ($image) +
+ +
+ @endif +
+ +

{{ $image ?: 'Kein Bild' }}

+
+
+
+
+ + {{-- Einleitung --}} +
+ Einleitung ({{ strtoupper($editLocale) }}) + +
+ + {{-- Leistungen --}} +
+
+ Leistungen und Besonderheiten + Punkt hinzufügen +
+
+ @foreach ($services as $i => $service) +
+
+ Punkt {{ $i + 1 }} + @if (count($services) > 1) + + @endif +
+ + +
+ @endforeach +
+
+ + {{-- Abschnitte der Markenseite --}} +
+
+ Abschnitte der Markenseite + Abschnitt hinzufügen +
+ +
+ Die Abschnitte erscheinen auf der Detailseite unter der Einleitung, in dieser Reihenfolge. + Eine Leerzeile trennt zwei Absätze. Die Vertiefung wird auf + der Seite als aufklappbarer Bereich dargestellt – leer lassen, wenn es keine gibt. +
+ + @if (empty($sections)) +

Noch keine Abschnitte.

+ @else +
+ @foreach ($sections as $i => $section) +
+
+ Abschnitt {{ $i + 1 }} +
+ + + +
+
+ + + + + + + +
+
+ +
+ +
+ +
+

Vertiefung – erscheint aufklappbar

+ + +
+
+ @endforeach +
+ @endif +
+ + {{-- Abschlussblock und Beschriftung des Terminanfrage-Widgets --}} +
+ Abschluss und Terminanfrage + + +

+ Steht auf dem schwebenden Button, solange man auf dieser Markenseite ist. Leer lassen für die + allgemeine Beschriftung. +

+ + + +
+ + {{-- Galerie --}} +
+
+ Bildergalerie +
+ Projekt + Inspiration + Werkstatt +
+
+ +
+ Projekt = tatsächlich ausgeführter Auftrag (erscheint auch unter „Referenzen“, solange dort nichts gepflegt ist). + Werkstatt = Einblick in Fertigung oder Montage des Herstellers. + Inspiration = Entwurf, Rendering oder KI-Darstellung. + Auf der Seite trägt jedes Bild seine Beschriftung – bitte nichts vermischen. +
+ + @if (empty($gallery)) +

Noch keine Bilder.

+ @else +
+ @foreach ($gallery as $i => $item) +
+ @if (!empty($item['image'])) +
+ +
+ @endif + + + Realisiertes Projekt + Inspiration / Entwurf + Aus der Werkstatt + + + + +
+ + +
+ + +
+ @endforeach +
+ @endif +
+ +
+ +
+ +
+ Speichern + Abbrechen +
+
+ @endif + + +
+ @forelse ($this->brands as $brand) +
+
+ @if ($brand->image) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $brand->getTranslation('name', $editLocale) }} + + @unless ($brand->is_published) + Entwurf + @endunless +
+

+ {{ $brand->getTranslation('tagline', $editLocale) ?: '–' }} · /marken/{{ $brand->slug }} +

+
+
+
+ + + + +
+
+ @empty +
+ + Keine Marken + Lege die erste Kernmarke mit dem Button oben an. +
+ @endforelse +
+
+
diff --git a/resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php b/resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php index d7d3927a..41e89afa 100644 --- a/resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php +++ b/resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php @@ -40,10 +40,13 @@ @endif -
@foreach ([['label' => 'Montag', 'open' => 'hoursMondayOpen', 'close' => 'hoursMondayClose'], ['label' => 'Dienstag', 'open' => 'hoursTuesdayOpen', 'close' => 'hoursTuesdayClose'], ['label' => 'Mittwoch', 'open' => 'hoursWednesdayOpen', 'close' => 'hoursWednesdayClose'], ['label' => 'Donnerstag', 'open' => 'hoursThursdayOpen', 'close' => 'hoursThursdayClose'], ['label' => 'Freitag', 'open' => 'hoursFridayOpen', 'close' => 'hoursFridayClose'], ['label' => 'Samstag', 'open' => 'hoursSaturdayOpen', 'close' => 'hoursSaturdayClose'], ['label' => 'Sonntag', 'open' => 'hoursSundayOpen', 'close' => 'hoursSundayClose']] as $row) - @php - $isClosedDay = empty($this->{$row['open']}) && empty($this->{$row['close']}); - @endphp -
+ {{-- Geoeffnet/Geschlossen wird clientseitig gerechnet, damit die Anzeige + sofort auf das Leeren per X reagiert. wire:model.live wuerde bei jeder + Aenderung einen Roundtrip ausloesen, der geleerte Felder wieder mit dem + alten Serverwert ueberschreibt. --}} +
{{ $row['label'] }} -
- +
-
- +
- - {{ $isClosedDay ? 'Geschlossen' : 'Geöffnet' }} +
@endforeach diff --git a/resources/views/livewire/admin/cms/content-index.blade.php b/resources/views/livewire/admin/cms/content-index.blade.php index 25bc797e..4d818745 100644 --- a/resources/views/livewire/admin/cms/content-index.blade.php +++ b/resources/views/livewire/admin/cms/content-index.blade.php @@ -1,6 +1,8 @@ fieldLabel($field, $group, $key); + } +} + layout('components.layouts.app'); title('CMS Inhalte'); state([ 'selectedGroup' => null, + 'selectedKey' => null, + 'showPreview' => true, + 'previewVersion' => 0, 'search' => '', 'editingId' => null, 'editingField' => null, @@ -108,12 +124,21 @@ $groups = computed(fn () => CmsContent::query() ->pluck('count', 'group') ->toArray()); +$tree = computed(fn () => app(CmsPageTree::class)->pages()); + +$currentSection = computed(fn () => $this->selectedGroup && $this->selectedKey + ? app(CmsPageTree::class)->section($this->selectedGroup, $this->selectedKey) + : null); + +$previewUrl = computed(fn () => app(CmsPageTree::class)->previewUrl($this->currentSection, $this->previewVersion)); + $flatContents = computed(function () { if (! $this->selectedGroup) { return collect(); } $contents = CmsContent::forGroup($this->selectedGroup) + ->when($this->selectedKey, fn ($q, $key) => $q->where('key', $key)) ->orderBy('order') ->get(); @@ -182,8 +207,40 @@ $selectGroup = function (string $group) { $this->editMediaId = null; $this->editingFieldType = 'text'; $this->selectedGroup = $group; + $this->selectedKey = null; }; +$selectSection = function (string $group, string $key) { + $this->selectGroup($group); + $this->selectedKey = $key; +}; + +// Sektion aus der Registry, die noch nicht in der Datenbank liegt: aus der Lang-Datei anlegen. +$createSection = function (string $group, string $key) { + $themeKey = app(CmsPageTree::class)->themeKey($group, $key); + + if ($themeKey === null) { + Flux::toast(variant: 'danger', heading: 'Keine Vorlage', text: 'Für diese Sektion gibt es keine Vorlage in der Sprachdatei.'); + + return; + } + + $seeder = app(CmsContentSeeder::class); + $seeder->onlySections = [$themeKey]; + $seeder->run(); + + app(CmsContentService::class)->clearCache($group); + + $this->selectSection($group, $key); + $this->previewVersion++; + + Flux::toast(variant: 'success', heading: 'Angelegt', text: 'Die Sektion wurde mit den Texten der Website angelegt.'); +}; + +$togglePreview = fn () => $this->showPreview = ! $this->showPreview; + +$reloadPreview = fn () => $this->previewVersion++; + $startFieldEdit = function (int $contentId, ?string $fieldKey = null) { $content = CmsContent::find($contentId); if (! $content) { @@ -371,6 +428,7 @@ $saveEdit = function () { $this->editValue = ''; $this->editingFieldType = 'text'; $this->editMediaId = null; + $this->previewVersion++; Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.'); }; @@ -435,6 +493,7 @@ $saveJsonModal = function () { $this->editingId = null; $this->editingField = null; $this->jsonItems = []; + $this->previewVersion++; Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.'); }; @@ -457,9 +516,8 @@ $cancelJsonModal = function () { ?> @php - $cmsGroupLabels = [ - 'legal' => 'Rechtliches', - ]; + $section = $this->currentSection; + $pageTitle = $selectedGroup ? ($this->tree[$section['page'] ?? '']['title'] ?? $selectedGroup) : null; @endphp
@@ -470,30 +528,85 @@ $cancelJsonModal = function () {
-
-
- - Seiten / Gruppen -
- @foreach ($this->groups as $group => $count) - - @endforeach -
+
+ {{-- Seitenbaum: Seiten in Menü-Reihenfolge, Sektionen von oben nach unten (config/cms_pages.php) --}} +
+ + @foreach ($this->tree as $pageKey => $page) +
+
+ {{ $page['title'] }} + @if ($page['path']) + {{ $page['path'] }} + @endif +
+ + @if ($pageKey === \App\Services\CmsPageTree::UNLISTED) +
+ {{ count($page['sections']) }} Sektionen stillgelegter Seiten +
+ @foreach ($page['sections'] as $item) + + @endforeach +
+
+ @else +
+ @foreach ($page['sections'] as $index => $item) + @php $isActive = $selectedGroup === $item['group'] && $selectedKey === $item['key']; @endphp + @if ($item['content_id']) + + @else +
+ {{ $index + 1 }} + {{ $item['title'] }} + +
+ @endif + @endforeach +
+ @endif +
+ @endforeach
-
+ {{-- Editor --}} +
@if ($selectedGroup) -
- {{ $cmsGroupLabels[$selectedGroup] ?? $selectedGroup }} +
+
+ {{ $section['title'] ?? ($selectedKey ?? $selectedGroup) }} + @if (! empty($section['description'])) + {{ $section['description'] }} + @endif +
+ @if ($pageTitle && $pageTitle !== ($section['title'] ?? null)) + {{ $pageTitle }} + + @endif + {{ $selectedGroup }}{{ $selectedKey ? '/' . $selectedKey : '' }} +
+
- + @if (! $selectedKey) + + @endif
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label) @endforeach
+ @if ($this->previewUrl) + {{ $showPreview ? 'Vorschau aus' : 'Vorschau' }} + @endif
@php $lastSection = null; @endphp
@forelse ($this->flatContents as $row) - @if ($row->section_key !== $lastSection) + @if (! $selectedKey && $row->section_key !== $lastSection) @php $lastSection = $row->section_key; @endphp
{{ $row->section_key }} @@ -522,18 +639,23 @@ $cancelJsonModal = function () { $editKey = $row->field_key !== null ? $row->content_id . ",'" . $row->field_key . "'" : $row->content_id . ',null'; + $fieldLabel = $row->field_key !== null + ? _cmsFieldLabel($row->field_key, $row->section_key ? $selectedGroup : null, $row->section_key) + : ($section['title'] ?? $row->section_key); @endphp
-
+
+ {{ $fieldLabel }} {{ $row->field_key ?? $row->section_key }} @php $typeColors = ['html' => 'amber', 'legal_html' => 'blue', 'image' => 'green', 'json' => 'violet', 'link' => 'rose']; + $typeLabels = ['html' => 'Formatierter Text', 'legal_html' => 'HTML', 'image' => 'Bild', 'json' => 'Liste', 'text' => 'Text']; $badgeColor = $typeColors[$row->type] ?? 'zinc'; @endphp - {{ $row->type }} + {{ $typeLabels[$row->type] ?? $row->type }}
- Seite auswählen - Wähle links eine Seite/Gruppe aus, um deren Inhalte zu bearbeiten. + Sektion auswählen + Links stehen die Seiten der Website mit ihren Abschnitten von oben nach unten. Wähle einen Abschnitt, um seine Texte und Bilder zu bearbeiten.
@endif
+ + {{-- Vorschau: die echte Website im Rahmen, springt zur gewählten Sektion und lädt nach dem Speichern neu --}} + @if ($selectedGroup && $showPreview && $this->previewUrl) +
+ +
+
+ Vorschau · {{ $section['path'] ?? '' }}{{ ! empty($section['anchor']) ? '#' . $section['anchor'] : '' }} +
+
+ + +
+
+ +
+
+ @endif
{{-- JSON-Editor: kein flux:modal — vermeidet ReferenceError fluxModal bei jedem Livewire-Render (z. B. bei legal_html). --}} @@ -664,8 +805,9 @@ $cancelJsonModal = function () { wire:click.stop >
- {{ $jsonEditingKey }} + {{ $section['title'] ?? $selectedGroup }}: {{ $editingField !== null ? _cmsFieldLabel($editingField, $selectedGroup, $selectedKey) : ($section['title'] ?? $jsonEditingKey) }} + {{ $jsonEditingKey }} · {{ $jsonIsStringArray ? 'Einfache Liste' : 'Strukturierte Einträge' }} ({{ count($jsonItems) }} Einträge) — {{ strtoupper($editLocale) }} @@ -707,7 +849,7 @@ $cancelJsonModal = function () {
— Kein Icon — @foreach ($this->availableIcons as $iconName) @@ -724,7 +866,7 @@ $cancelJsonModal = function () {
@elseif ($isImageField)
- +
@if (is_string($fieldValue) && $fieldValue !== '')
@@ -747,18 +889,18 @@ $cancelJsonModal = function () { @elseif ($isRichText)
@elseif ($isNestedJson)
@else + label="{{ _cmsFieldLabel($field, $selectedGroup, $selectedKey) }}" /> @endif @endforeach
diff --git a/resources/views/livewire/admin/cms/dashboard.blade.php b/resources/views/livewire/admin/cms/dashboard.blade.php index 42f08d31..685a6d0d 100644 --- a/resources/views/livewire/admin/cms/dashboard.blade.php +++ b/resources/views/livewire/admin/cms/dashboard.blade.php @@ -1,7 +1,8 @@ [ 'contents' => CmsContent::count(), 'groups' => CmsContent::distinct()->pluck('group')->count(), - 'projects' => CmsProject::count(), - 'projects_published' => CmsProject::published()->count(), + 'references' => CmsReference::count(), + 'references_published' => CmsReference::published()->count(), 'articles' => CmsArticle::count(), 'articles_published' => CmsArticle::published()->count(), + 'brands' => CmsBrand::count(), + 'brands_published' => CmsBrand::published()->count(), 'media' => CmsMedia::count(), ]); @@ -24,7 +27,7 @@ $stats = computed(fn () => [
CMS - Inhalte, Projekte und Medien verwalten. + Inhalte, Referenzen, Marken, Magazin und Medien verwalten.
@@ -40,13 +43,13 @@ $stats = computed(fn () => [ - +
- +
- {{ $this->stats['projects'] }} - Projekte ({{ $this->stats['projects_published'] }} veröffentlicht) + {{ $this->stats['references'] }} + Referenzen ({{ $this->stats['references_published'] }} veröffentlicht)
@@ -64,6 +67,18 @@ $stats = computed(fn () => [
+ + +
+ +
+ {{ $this->stats['brands'] }} + Kernmarken ({{ $this->stats['brands_published'] }} veröffentlicht) +
+
+
+
+
@@ -92,7 +107,7 @@ $stats = computed(fn () => [

  • Inhalte – Texte, Abschnitte und strukturierte Daten der Website (z. B. Startseite, Netzwerk, rechtliche Texte). Inhalte sind in Gruppen organisiert, sodass zusammengehörige Felder gebündelt bearbeitet werden können.
  • -
  • Projekte – Immobilien-Referenzobjekte mit Bildern, Preisen und Beschreibungen. Jedes Projekt kann als Entwurf angelegt und später veröffentlicht werden.
  • +
  • Referenzen – Realisierte Projekte für die Seite /referenzen mit Titelbild, weiteren Bildern und den beteiligten Marken. Nur ausgeführte Aufträge; Entwürfe gehören in die Galerie der Marke.
  • Magazin – Redaktionelle Artikel (z. B. über Dubai-Immobilien, Supply-Chain oder Einrichtung). Artikel unterstützen Kategorien, Autoren und Lesezeiten.
  • Medien – Zentrale Bild- und Dateiverwaltung. Alle Uploads landen hier und können in jedem Modul wiederverwendet werden.
diff --git a/resources/views/livewire/admin/cms/display-version-editor.blade.php b/resources/views/livewire/admin/cms/display-version-editor.blade.php index 6b7b4ba6..79c5bb45 100644 --- a/resources/views/livewire/admin/cms/display-version-editor.blade.php +++ b/resources/views/livewire/admin/cms/display-version-editor.blade.php @@ -190,8 +190,10 @@ @if($mediaType === 'image') - + + @error('mediaDuration') {{ $message }} @enderror @endif @endif diff --git a/resources/views/livewire/admin/cms/partials/version-editor-b2in.blade.php b/resources/views/livewire/admin/cms/partials/version-editor-b2in.blade.php index fde7dc72..f5347fc7 100644 --- a/resources/views/livewire/admin/cms/partials/version-editor-b2in.blade.php +++ b/resources/views/livewire/admin/cms/partials/version-editor-b2in.blade.php @@ -65,7 +65,7 @@
{{ $item->content['subline'] ?? '' }} @if(($item->content['media_type'] ?? 'image') === 'image') - {{ $item->content['duration_seconds'] ?? 10 }}s + {{ \App\Livewire\Admin\Cms\DisplayVersionEditor::formatDuration($item->content['duration_seconds'] ?? 10) }}s @endif @if($formatStatus = $this->displayMediaFormatStatus($item->content['media_url'] ?? '')) {{ $formatStatus['description'] }} diff --git a/resources/views/livewire/admin/cms/projects-index.blade.php b/resources/views/livewire/admin/cms/projects-index.blade.php index c39c53d9..a94eaa10 100644 --- a/resources/views/livewire/admin/cms/projects-index.blade.php +++ b/resources/views/livewire/admin/cms/projects-index.blade.php @@ -613,7 +613,7 @@ $setActiveTab = function (string $tab): void {
- +
diff --git a/resources/views/livewire/admin/cms/references-index.blade.php b/resources/views/livewire/admin/cms/references-index.blade.php new file mode 100644 index 00000000..bf7c6de3 --- /dev/null +++ b/resources/views/livewire/admin/cms/references-index.blade.php @@ -0,0 +1,367 @@ + '', + 'showForm' => false, + 'editingId' => null, + 'editLocale' => 'de', + 'referenceTitle' => '', + 'description' => '', + 'location' => '', + 'year' => '', + 'image' => '', + 'gallery' => [], + 'brandSlugs' => [], + 'is_published' => true, + 'order' => 0, +]); + +on(['media-selected' => function ($mediaId, $url, $field) { + $media = $mediaId ? CmsMedia::find($mediaId) : null; + + if (! $media) { + return; + } + + if ($field === 'reference_image') { + $this->image = $media->filename; + } + + if (str_starts_with($field, 'reference_gallery:')) { + $index = (int) substr($field, strlen('reference_gallery:')); + + if (isset($this->gallery[$index])) { + $this->gallery[$index]['image'] = $media->filename; + } + } +}]); + +$references = computed( + fn () => CmsReference::query() + ->when($this->search, fn ($q, $s) => $q->where('location', 'like', "%{$s}%") + ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(title, '$.de')) LIKE ?", ["%{$s}%"])) + ->ordered() + ->get(), +); + +$brands = computed(fn () => CmsBrand::query()->ordered()->get()); + +$openCreate = function () { + $this->editingId = null; + $this->reset(['referenceTitle', 'description', 'location', 'year', 'image']); + $this->gallery = []; + $this->brandSlugs = []; + $this->is_published = true; + $this->order = (CmsReference::max('order') ?? 0) + 1; + $this->showForm = true; +}; + +$loadLocaleFields = function (CmsReference $reference, string $locale) { + $this->referenceTitle = $reference->getTranslation('title', $locale) ?? ''; + $this->description = $reference->getTranslation('description', $locale) ?? ''; +}; + +$openEdit = function (int $id) { + $reference = CmsReference::find($id); + + if (! $reference) { + return; + } + + $this->editingId = $id; + $this->location = $reference->location ?? ''; + $this->year = $reference->year ?? ''; + $this->image = $reference->image ?? ''; + $this->gallery = CmsReference::normalizeGallery($reference->gallery); + $this->brandSlugs = array_values(array_map('strval', $reference->brand_slugs ?? [])); + $this->is_published = $reference->is_published; + $this->order = $reference->order ?? 0; + + $this->loadLocaleFields($reference, $this->editLocale); + $this->showForm = true; +}; + +$switchLocale = function (string $locale) { + $this->editLocale = $locale; + + if ($this->editingId) { + $reference = CmsReference::find($this->editingId); + + if ($reference) { + $this->loadLocaleFields($reference, $locale); + } + } +}; + +$addGalleryImage = fn () => $this->gallery[] = ['image' => '', 'caption' => '']; + +$removeGalleryImage = function (int $index) { + unset($this->gallery[$index]); + $this->gallery = array_values($this->gallery); +}; + +$moveGalleryImage = function (int $index, int $direction) { + $target = $index + $direction; + + if (! isset($this->gallery[$index], $this->gallery[$target])) { + return; + } + + [$this->gallery[$index], $this->gallery[$target]] = [$this->gallery[$target], $this->gallery[$index]]; +}; + +$save = function () { + $validated = validator([ + 'referenceTitle' => $this->referenceTitle, + 'description' => $this->description, + 'location' => $this->location, + 'year' => $this->year, + 'image' => $this->image, + 'brandSlugs' => $this->brandSlugs, + 'is_published' => $this->is_published, + 'order' => $this->order, + ], [ + 'referenceTitle' => 'required|string|max:255', + 'description' => 'nullable|string|max:4000', + 'location' => 'nullable|string|max:255', + 'year' => 'nullable|string|max:20', + 'image' => 'nullable|string|max:500', + 'brandSlugs' => 'array', + 'brandSlugs.*' => 'string|exists:cms_brands,slug', + 'is_published' => 'boolean', + 'order' => 'integer|min:0', + ], [ + 'referenceTitle.required' => 'Bitte einen Titel angeben.', + 'brandSlugs.*.exists' => 'Eine der gewählten Marken gibt es nicht mehr.', + ])->validate(); + + $reference = $this->editingId + ? CmsReference::findOrFail($this->editingId) + : CmsReference::query()->make(); + + $reference->setTranslation('title', $this->editLocale, trim($validated['referenceTitle'])); + $reference->setTranslation('description', $this->editLocale, trim((string) ($validated['description'] ?? ''))); + $reference->location = trim((string) ($validated['location'] ?? '')) ?: null; + $reference->year = trim((string) ($validated['year'] ?? '')) ?: null; + $reference->image = trim((string) ($validated['image'] ?? '')) ?: null; + $reference->gallery = CmsReference::normalizeGallery($this->gallery); + $reference->brand_slugs = array_values(array_unique($validated['brandSlugs'])); + $reference->is_published = $validated['is_published']; + $reference->order = $validated['order']; + $reference->save(); + + $this->showForm = false; + $this->editingId = null; + + Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Referenz wurde erfolgreich gespeichert.'); +}; + +$togglePublished = function (int $id) { + $reference = CmsReference::findOrFail($id); + $reference->update(['is_published' => ! $reference->is_published]); + + Flux::toast(heading: 'Status geändert', text: $reference->is_published ? 'Veröffentlicht' : 'Entwurf'); +}; + +$deleteReference = function (int $id) { + CmsReference::findOrFail($id)->delete(); + + Flux::toast(variant: 'success', heading: 'Gelöscht', text: 'Referenz wurde entfernt.'); +}; + +$cancelForm = function () { + $this->showForm = false; + $this->editingId = null; +}; + +?> + +
+
+
+ Referenzen + Realisierte Projekte für die Seite /referenzen. Nur ausgeführte Aufträge – Entwürfe und Visualisierungen gehören in die Galerie der jeweiligen Marke. +
+
+ @foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label) + {{ strtoupper($code) }} + @endforeach + Neue Referenz +
+
+ +
+ +
+ + @if ($showForm) + +
+ {{ $editingId ? 'Referenz bearbeiten' : 'Neue Referenz' }} +
+ @foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label) + {{ strtoupper($code) }} + @endforeach +
+
+ +
+
+ + @error('referenceTitle') {{ $message }} @enderror +
+ + +
+ +
+ +
+ + {{-- Beteiligte Marken --}} +
+ Beteiligte Marken + Jede gewählte Marke wird unter der Referenz genannt und auf ihre Markenseite verlinkt. +
+ @foreach ($this->brands as $brand) + + @endforeach +
+ @error('brandSlugs.*') {{ $message }} @enderror +
+ + {{-- Titelbild --}} +
+ +
+ @if ($image) +
+ +
+ @endif +
+ + +
+
+
+ + {{-- Weitere Bilder --}} +
+
+ Weitere Bilder + Bild hinzufügen +
+ + @if (empty($gallery)) +

Noch keine weiteren Bilder.

+ @else +
+ @foreach ($gallery as $i => $item) +
+ @if (!empty($item['image'])) +
+ +
+ @endif + + + +
+ +
+ + + +
+
+ + +
+ @endforeach +
+ @endif +
+ +
+ +
+ +
+ Speichern + Abbrechen +
+
+ @endif + + +
+ @forelse ($this->references as $reference) +
+
+ @if ($reference->image) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $reference->getTranslation('title', $editLocale) ?: $reference->getTranslation('title', 'de') }} + + @foreach ($reference->brand_slugs ?? [] as $slug) + {{ $slug }} + @endforeach + @unless ($reference->is_published) + Entwurf + @endunless +
+

+ {{ $reference->location ?: '–' }} · {{ $reference->year ?: '–' }} · {{ count(CmsReference::normalizeGallery($reference->gallery)) + ($reference->image ? 1 : 0) }} Bilder +

+
+
+
+ + + +
+
+ @empty +
+ + Keine Referenzen + Solange hier nichts steht, zeigt /referenzen die als „Realisiertes Projekt“ markierten Bilder aus den Markengalerien. +
+ @endforelse +
+
+
diff --git a/resources/views/livewire/web/components/sections/about-hero.blade.php b/resources/views/livewire/web/components/sections/about-hero.blade.php index 05cecd8f..896130cf 100644 --- a/resources/views/livewire/web/components/sections/about-hero.blade.php +++ b/resources/views/livewire/web/components/sections/about-hero.blade.php @@ -1,4 +1,4 @@ -
+
diff --git a/resources/views/livewire/web/components/sections/audience-switch.blade.php b/resources/views/livewire/web/components/sections/audience-switch.blade.php new file mode 100644 index 00000000..d89f5dfb --- /dev/null +++ b/resources/views/livewire/web/components/sections/audience-switch.blade.php @@ -0,0 +1,51 @@ +@php + if (!function_exists('renderHeroIcon')) { + function renderHeroIcon($iconName, $style = 'outline', $color = 'text-secondary') + { + $iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg"); + $fallbackPath = public_path('heroicons/optimized/24/outline/sparkles.svg'); + + if (file_exists($iconPath)) { + return str_replace(' +
+
+

{!! $content['title'] ?? '' !!}

+ @if (!empty($content['subtitle'])) +

{{ $content['subtitle'] }}

+ @endif +
+ +
+ @foreach ($content['cards'] ?? [] as $index => $card) +
+
+ {!! renderHeroIcon($card['icon'] ?? 'home', 'outline', 'text-secondary-foreground') !!} +
+ +

{{ $card['title'] ?? '' }}

+

{!! $card['description'] ?? '' !!}

+ + @if (!empty($card['button_text'])) +
+ + {{ $card['button_text'] }} + +
+ @endif +
+ @endforeach +
+
+
diff --git a/resources/views/livewire/web/components/sections/brand-detail.blade.php b/resources/views/livewire/web/components/sections/brand-detail.blade.php new file mode 100644 index 00000000..e0941398 --- /dev/null +++ b/resources/views/livewire/web/components/sections/brand-detail.blade.php @@ -0,0 +1,347 @@ +{{-- Detailseiten-Vorlage laut umsetzung-inhalte.md §5: + Header (Logo + Claim + Stimmungsbild) → Einleitung → Leistungen → Galerie → CTA --}} +
+ {{-- 1. Header --}} +
+ +
+ + {{-- 2. Einleitung --}} + @if (!empty($brand['description'])) +
+
+
+ {!! $brand['description'] !!} +
+
+
+ @endif + + {{-- 2b. Abschnitte der Markenseite. Die als Vertiefung gelieferten Blöcke sind aufklappbar, + damit die Seite schnell erfassbar bleibt, ohne dass Inhalt verloren geht. --}} + @if (!empty($brand['sections'])) +
+ +
+ @endif + + {{-- 3. Leistungen --}} + @if (!empty($brand['services'])) +
+
+

{{ __('ui.brand_services') }}

+ +
+ @foreach ($brand['services'] as $index => $service) +
+

{{ $service['title'] ?? '' }}

+

{{ $service['description'] ?? '' }}

+
+ @endforeach +
+
+
+ @endif + + {{-- 4. Galerie. Realisierte Projekte, Einblicke in die Werkstatt und Visualisierungen stehen in + einem Raster, unterschieden nur durch das Badge – der Rubrikwechsel ist ausgesetzt + (Abstimmung 22.09., Nachtrag 17). Reihenfolge: Projekte, Werkstatt, Visualisierungen. + Überschrift und Zeile darunter richten sich nach dem, was die Galerie enthält (Nachtrag 19): + nur Visualisierungen → „Inspirationen und Entwürfe“, nur Projekte → „Referenzen“, Projekte + und Werkstatt → „Referenzen und Einblicke“, mit Visualisierungen → „Projekte und Entwürfe“. --}} + @php + $galleryItems = array_merge($gallery['projects'] ?? [], $gallery['company'] ?? [], $gallery['inspirations'] ?? []); + + $galleryHasReal = ! empty($gallery['projects']) || ! empty($gallery['company']); + $galleryHasDrafts = ! empty($gallery['inspirations']); + $galleryVariant = match (true) { + ! $galleryHasReal => 'inspirations', + $galleryHasDrafts => 'mixed', + ! empty($gallery['company']) => 'projects_company', + default => 'projects', + }; + + $galleryLightbox = collect($galleryItems)->map(fn (array $item) => [ + 'src' => theme_image_url($item['image']), + 'caption' => $item['caption'] ?? '', + 'badge' => \App\Models\CmsBrand::galleryBadge($item['category']), + ])->values()->all(); + @endphp + + @if ($galleryItems) +
+
+
+

{{ __('ui.brand_gallery_title_'.$galleryVariant) }}

+

{{ __('ui.brand_gallery_intro_'.$galleryVariant) }}

+
+ +
+ @foreach ($galleryItems as $index => $item) +
+ + @if (!empty($item['caption'])) +
{{ $item['caption'] }}
+ @endif +
+ @endforeach +
+
+ + {{-- Lightbox: ins body geteleportet, damit keine Eltern-Constraints stören --}} + +
+ @endif + + {{-- 5. Abschluss. Der Block schliesst die Seite ab und traegt den Claim aus der Zulieferung. + Der durchgehende Verlauf steckt in `.cta-band` (shared-styles.css) – als Utility-Klassen + waere er nicht ueber alle Themes hinweg zu halten. --}} +
+
+ {{-- Haarlinie als Auftakt, laeuft zu beiden Seiten aus. --}} + + +

+ {{ $brand['cta_title'] ?: __('ui.brand_cta_title', ['name' => $brand['name']]) }} +

+

+ {{ $brand['cta_text'] ?: __('ui.brand_cta_text') }} +

+ +
+ {{-- Oeffnet das Widget mit dieser Marke als Anliegen. --}} + + {{ __('ui.request_appointment') }} + + + + @if (!empty($brand['website_url'])) + + {{ __('ui.brand_website') }} + + + @endif +
+ + @if (!empty($brand['showroom'])) +

+ + {{ $brand['showroom'] }} +

+ @endif +
+
+ + {{-- Weitere Marken --}} + @if (!empty($otherBrands)) +
+
+

{{ __('ui.other_brands') }}

+ + {{-- Drei weitere Marken: nebeneinander ab lg, darunter zweispaltig. --}} + +
+
+ @endif +
diff --git a/resources/views/livewire/web/components/sections/brand-overview.blade.php b/resources/views/livewire/web/components/sections/brand-overview.blade.php new file mode 100644 index 00000000..79c8b17c --- /dev/null +++ b/resources/views/livewire/web/components/sections/brand-overview.blade.php @@ -0,0 +1,62 @@ +{{-- Übersichtsseite /marken: je Marke Logo, Bild, Claim, Text, drei Merkmale und der Weg + zur Detailseite. Jede Marke steht in einem eigenen Rahmen, damit klar ist, was zusammengehört. + Abwechselnde Bildseite, damit die Liste nicht monoton wird. --}} +
+
+ @foreach ($brands as $index => $brand) +
+
+
+ @if (!empty($brand['image'])) + {{ $brand['name'] }} + @else + {{-- Platzhalter, solange die Bildmotive fehlen (offene Zulieferung). --}} +
+ {{ $brand['name'] }} +
+ @endif +
+ +
+ {{-- Feste Box statt fester Hoehe: die Markenlogos haben sehr unterschiedliche + Seitenverhaeltnisse und wirken sonst ungleich gross. Die optische Feinabstimmung + steckt in den Zeichenflaechen der Dateien, nicht hier – siehe BrandLogoSeeder. --}} + @if (!empty($brand['logo'])) + {{ $brand['name'] }} + @endif + +

{{ $brand['name'] }}

+

{{ $brand['tagline'] }}

+ + @foreach (preg_split('/\R{2,}/', trim((string) ($brand['summary'] ?: $brand['teaser']))) as $i => $paragraph) +

{!! nl2br(e($paragraph)) !!}

+ @endforeach + + @if (!empty($brand['highlights'])) +
    + @foreach ($brand['highlights'] as $i => $highlight) +
  • + + {{ is_array($highlight) ? ($highlight['title'] ?? '') : $highlight }} +
  • + @endforeach +
+ @endif + + @if (!empty($brand['has_detail_page'])) + + {{ __('ui.more_about', ['name' => $brand['name']]) }} + + @endif +
+
+
+ @endforeach +
+
diff --git a/resources/views/livewire/web/components/sections/c-t-a-section.blade.php b/resources/views/livewire/web/components/sections/c-t-a-section.blade.php index 4e661196..c1ad03c0 100644 --- a/resources/views/livewire/web/components/sections/c-t-a-section.blade.php +++ b/resources/views/livewire/web/components/sections/c-t-a-section.blade.php @@ -1,4 +1,4 @@ -
+

@@ -9,13 +9,19 @@ {{ $content['subtitle'] }}

+ {{-- Weisser Button wie auf den Markenseiten: der Block liegt standardmaessig auf dem + Abschlussband (`cta-band`), dort ginge ein Button im Hauptton unter. --}}

diff --git a/resources/views/livewire/web/components/sections/core-brands.blade.php b/resources/views/livewire/web/components/sections/core-brands.blade.php new file mode 100644 index 00000000..675261ee --- /dev/null +++ b/resources/views/livewire/web/components/sections/core-brands.blade.php @@ -0,0 +1,57 @@ +
+
+
+

{!! $content['title'] ?? '' !!}

+ @if (!empty($content['subtitle'])) +

{{ $content['subtitle'] }}

+ @endif +
+ + {{-- Vier Kernmarken als 2×2-Raster; vier nebeneinander wäre zu schmal (Abstimmung 15.09.). --}} +
+ @foreach ($brands as $brand) +
+ @if (!empty($brand['image'])) + {{ $brand['name'] }} + @else + {{-- Platzhalter, solange die Bildmotive fehlen (offene Zulieferung). --}} +
+ {{ $brand['name'] }} +
+ @endif + +
+ {{-- Feste Box statt fester Hoehe: die Markenlogos haben sehr unterschiedliche + Seitenverhaeltnisse und wirken sonst ungleich gross. Die optische Feinabstimmung + steckt in den Zeichenflaechen der Dateien, nicht hier – siehe BrandLogoSeeder. --}} + @if (!empty($brand['logo'])) + {{ $brand['name'] }} + @endif + +

{{ $brand['name'] }}

+

{{ $brand['tagline'] }}

+

{!! $brand['teaser'] !!}

+ + @if (!empty($brand['has_detail_page'])) + + {{ __('ui.more_about', ['name' => $brand['name']]) }} + + + @endif +
+
+ @endforeach +
+ + {{-- Die Startseite bleibt ein Teaser – die vollstaendige Darstellung steht auf /marken. --}} + +
+
diff --git a/resources/views/livewire/web/components/sections/display-feed.blade.php b/resources/views/livewire/web/components/sections/display-feed.blade.php new file mode 100644 index 00000000..332506d5 --- /dev/null +++ b/resources/views/livewire/web/components/sections/display-feed.blade.php @@ -0,0 +1,37 @@ +{{-- Livewire braucht ein Wurzelelement. Solange kein Display veröffentlicht ist, bleibt es leer – + lieber gar keine Sektion als ein Rahmen mit ewigem Ladebalken. --}} +
+ @if ($displayId !== null) +
+
+
+
+

{!! $content['title'] ?? '' !!}

+

{{ $content['text'] ?? '' }}

+ + @if (!empty($content['note'])) +

{{ $content['note'] }}

+ @endif + + @if ($showLink && !empty($content['link_text'])) + + {{ $content['link_text'] }} + + @endif +
+ + {{-- 9:16 wie das physische Display im Showroom --}} +
+
+ +
+
+
+
+
+@endif +
diff --git a/resources/views/livewire/web/components/sections/f-a-q.blade.php b/resources/views/livewire/web/components/sections/f-a-q.blade.php index 665b18ad..8d9e84d4 100644 --- a/resources/views/livewire/web/components/sections/f-a-q.blade.php +++ b/resources/views/livewire/web/components/sections/f-a-q.blade.php @@ -1,6 +1,6 @@ @php($faqCounter = 0)
-
+
{{-- Section Title --}}
@@ -25,34 +25,39 @@
-
-
-

- {{ $faq['answer'] }} -

+ {{-- 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. --}} +
+
+
+
+

+ {{ $faq['answer'] }} +

+
+
@@ -80,34 +85,39 @@
-
-
-

- {{ $faq['answer'] }} -

+ {{-- 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. --}} +
+
+
+
+

+ {{ $faq['answer'] }} +

+
+
diff --git a/resources/views/livewire/web/components/sections/founder-bar.blade.php b/resources/views/livewire/web/components/sections/founder-bar.blade.php index 123ff86a..91c7e677 100644 --- a/resources/views/livewire/web/components/sections/founder-bar.blade.php +++ b/resources/views/livewire/web/components/sections/founder-bar.blade.php @@ -1,4 +1,4 @@ -
+
@if(isset($content['image'])) diff --git a/resources/views/livewire/web/components/sections/hero-slider.blade.php b/resources/views/livewire/web/components/sections/hero-slider.blade.php index 20656411..bdb9ff8d 100644 --- a/resources/views/livewire/web/components/sections/hero-slider.blade.php +++ b/resources/views/livewire/web/components/sections/hero-slider.blade.php @@ -1,139 +1,102 @@ +{{-- Hero des Entwurfs (Nachtrag 19): Rahmen mit feiner Goldkante und grünen Flanken, links + Text mit einem Button, rechts die Slideshow. Das Bild hat ein festes Seitenverhältnis – + das Motiv wird auf großen Bildschirmen nicht mehr unten beschnitten (Briefing Punkt 2). + Der Wechsel läuft langsam (Überblendung 1,4 s, Standard alle 7 s), pausiert bei Hover und + bleibt bei „reduzierter Bewegung“ stehen. --}} +
+
-
-
- -
- @if(!empty($content['slides'])) - @foreach($content['slides'] as $index => $slide) -
- {{ $slide['image_alt'] }} -
-
- @endforeach - @endif -
+
+ + - -
-

- {!! $content['title'] !!} -

-

{!! $content['subtitle'] !!}

- {{ $content['cta1_text'] }} -
+ {{-- Rechts wenig Innenabstand: das Bild liegt über der grünen Flanke, wie in der Visualisierung. --}} +
+ {{-- Text --}} +
+ @if (!empty($content['eyebrow'])) +

{{ $content['eyebrow'] }}

+ @endif - - @if(!empty($content['slides']) && count($content['slides']) > 1) -
-
- @foreach($content['slides'] as $index => $slide) - - @endforeach +

{!! $content['title'] ?? '' !!}

+ + @if (!empty($content['subtitle'])) +

{!! $content['subtitle'] !!}

+ @endif + + +
+ + {{-- Slideshow --}} +
+
+
+ @forelse ($slides as $index => $slide) +
+ {{ $slide['alt'] }} 0) loading="lazy" @endif /> + @if ($slide['label'] !== '') +
+ {{ $slide['label'] }} +
+ @endif +
+ @empty +
+ {{ __('ui.hero_slider.empty') }} +
+ @endforelse +
+
+
+
-
- @endif - - @if(!empty($content['slides']) && count($content['slides']) > 1) - - - - @endif -
-
- - + {{-- Punkte unter dem Rahmen --}} + @if (count($slides) > 1) +
+ @foreach ($slides as $index => $slide) + + @endforeach +
+ @endif +
+
diff --git a/resources/views/livewire/web/components/sections/hero.blade.php b/resources/views/livewire/web/components/sections/hero.blade.php index 4262e446..ff45714b 100644 --- a/resources/views/livewire/web/components/sections/hero.blade.php +++ b/resources/views/livewire/web/components/sections/hero.blade.php @@ -1,4 +1,4 @@ -
+
{{-- Left Content --}} @@ -16,9 +16,9 @@ {{ $content['cta1_text'] }} - + {{ $content['cta2_text'] }} - +
@if(isset($content['stats'])) diff --git a/resources/views/livewire/web/components/sections/image-break.blade.php b/resources/views/livewire/web/components/sections/image-break.blade.php index 93ff62fa..2885e62e 100644 --- a/resources/views/livewire/web/components/sections/image-break.blade.php +++ b/resources/views/livewire/web/components/sections/image-break.blade.php @@ -1,5 +1,5 @@ @if (!empty($content)) -
+
+
+
+

{!! $content['title'] ?? '' !!}

+

{{ $content['text'] ?? '' }}

+ + @if (!empty($content['link_text'])) + + {{ $content['link_text'] }} + + @endif +
+
+
diff --git a/resources/views/livewire/web/components/sections/magazin-detail.blade.php b/resources/views/livewire/web/components/sections/magazin-detail.blade.php index 747f4c09..8f82c2bc 100644 --- a/resources/views/livewire/web/components/sections/magazin-detail.blade.php +++ b/resources/views/livewire/web/components/sections/magazin-detail.blade.php @@ -2,7 +2,7 @@

- + {{ $content['cta_button'] }} diff --git a/resources/views/livewire/web/components/sections/magazin-list.blade.php b/resources/views/livewire/web/components/sections/magazin-list.blade.php index bd8aca96..f0851100 100644 --- a/resources/views/livewire/web/components/sections/magazin-list.blade.php +++ b/resources/views/livewire/web/components/sections/magazin-list.blade.php @@ -1,4 +1,4 @@ -
+

@@ -32,7 +32,7 @@

- + {!! $post['title'] !!}

@@ -44,14 +44,14 @@
- + diff --git a/resources/views/livewire/web/components/sections/marken-cta.blade.php b/resources/views/livewire/web/components/sections/marken-cta.blade.php new file mode 100644 index 00000000..55732b6e --- /dev/null +++ b/resources/views/livewire/web/components/sections/marken-cta.blade.php @@ -0,0 +1,22 @@ +
+ +
diff --git a/resources/views/livewire/web/components/sections/marken-growth.blade.php b/resources/views/livewire/web/components/sections/marken-growth.blade.php new file mode 100644 index 00000000..617c654f --- /dev/null +++ b/resources/views/livewire/web/components/sections/marken-growth.blade.php @@ -0,0 +1,9 @@ +{{-- Optionaler Block laut Spec – über das CMS-Feld `enabled` abschaltbar. --}} +@if ($content['enabled'] ?? true) +
+
+

{!! $content['title'] ?? '' !!}

+

{{ $content['text'] ?? '' }}

+
+
+@endif diff --git a/resources/views/livewire/web/components/sections/marken-network.blade.php b/resources/views/livewire/web/components/sections/marken-network.blade.php new file mode 100644 index 00000000..0fd737b3 --- /dev/null +++ b/resources/views/livewire/web/components/sections/marken-network.blade.php @@ -0,0 +1,46 @@ +@php + if (!function_exists('renderHeroIcon')) { + function renderHeroIcon($iconName, $style = 'outline', $color = 'text-secondary') + { + $iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg"); + $fallbackPath = public_path('heroicons/optimized/24/outline/sparkles.svg'); + + if (file_exists($iconPath)) { + return str_replace(' +
+
+

{!! $content['title'] ?? '' !!}

+ @foreach ($content['paragraphs'] ?? [] as $i => $paragraph) +

+ {!! $paragraph !!} +

+ @endforeach +
+ + @if (!empty($content['points'])) +
+ @foreach ($content['points'] as $index => $point) +
+
+ {!! renderHeroIcon($point['icon'] ?? 'user', 'outline', 'text-secondary-foreground') !!} +
+

{{ $point['title'] ?? '' }}

+

{{ $point['description'] ?? '' }}

+
+ @endforeach +
+ @endif +
+
diff --git a/resources/views/livewire/web/components/sections/origin-section.blade.php b/resources/views/livewire/web/components/sections/origin-section.blade.php new file mode 100644 index 00000000..dcaf0d18 --- /dev/null +++ b/resources/views/livewire/web/components/sections/origin-section.blade.php @@ -0,0 +1,131 @@ +@php + $hub = $content['hub'] ?? []; + $hubX = (int) ($hub['x'] ?? 400); + $hubY = (int) ($hub['y'] ?? 175); + $steps = array_values(array_filter($content['steps'] ?? [], fn ($step) => is_array($step) && ! empty($step['title']))); +@endphp + +
+
+
+

{!! $content['title'] ?? '' !!}

+ @if (!empty($content['subtitle'])) +

{{ $content['subtitle'] }}

+ @endif +
+ + {{-- Ohne gepflegten Fertigungsort bleibt nur die Überschrift und der Ablauf-Block. --}} + @if ($this->locations !== []) +
+ {{-- Schematische Karte: Bielefeld als Drehscheibe, die Standorte als Speichen. + Keine Geografie, nur Richtung und Entfernung. Positionen aus OriginSection::MAP_POSITIONS, + eine Marke ohne Position steht nur in den Kacheln. Unter md tragen die Kacheln alles. --}} + + + {{-- Eine Kachel je Marke mit Fertigungsort --}} +
+ @foreach ($this->locations as $location) +
+ + + {{ $location['city'] }} + +

{{ $location['brand'] }}

+ @if (!empty($location['role'])) +

{{ $location['role'] }}

+ @endif +
+ @if (!empty($location['distance'])) + {{ $location['distance'] }} {{ $content['distance_suffix'] ?? '' }} + @endif + @if (!empty($location['link']) && !empty($content['link_text'])) + + {{ $content['link_text'] }} + + @endif +
+
+ @endforeach +
+
+ @endif + + {{-- Gefertigt oder vertrieben? Der Weg eines Maßmöbels am Beispiel CABINET. --}} + @if ($steps !== []) +
+
+

{!! $content['steps_title'] ?? '' !!}

+ @if (!empty($content['steps_text'])) +

{{ $content['steps_text'] }}

+ @endif +
+ +
    + @foreach ($steps as $index => $step) +
  1. + + {{ $index + 1 }} + +
    +

    {{ $step['title'] }}

    + @if (!empty($step['description'])) +

    {{ $step['description'] }}

    + @endif +
    +
  2. + @endforeach +
+
+ @endif +
+
diff --git a/resources/views/livewire/web/components/sections/our-story.blade.php b/resources/views/livewire/web/components/sections/our-story.blade.php index d5793a94..633caacc 100644 --- a/resources/views/livewire/web/components/sections/our-story.blade.php +++ b/resources/views/livewire/web/components/sections/our-story.blade.php @@ -1,4 +1,4 @@ -
+

diff --git a/resources/views/livewire/web/components/sections/our-values.blade.php b/resources/views/livewire/web/components/sections/our-values.blade.php index d0d059dc..fb058f8b 100644 --- a/resources/views/livewire/web/components/sections/our-values.blade.php +++ b/resources/views/livewire/web/components/sections/our-values.blade.php @@ -21,7 +21,7 @@ function renderHeroIcon($iconName, $style = 'outline') { } } @endphp -
+

{!! $content['title'] !!}

diff --git a/resources/views/livewire/web/components/sections/page-hero.blade.php b/resources/views/livewire/web/components/sections/page-hero.blade.php new file mode 100644 index 00000000..6d200f9f --- /dev/null +++ b/resources/views/livewire/web/components/sections/page-hero.blade.php @@ -0,0 +1,36 @@ +{{-- Seitenkopf für Unterseiten – abgesetzte graue Fläche wie der Hero auf Startseite und + Netzwerkseite, damit er sich klar von den Blöcken darunter absetzt. --}} +
+
+ {{-- Breadcrumb: Home → Marken --}} + + + @if (!empty($content['image'])) + {{-- 3:2 statt hälftig geteilt – der Text bleibt der Hauptdarsteller, das Bild begleitet ihn nur. --}} +
+
+

{!! $content['title'] ?? '' !!}

+

{{ $content['text'] ?? '' }}

+
+ +
+ {{ $content['image_alt'] ?? '' }} +
+
+
+ @else + {{-- Ohne Motiv bleibt der Textblock allein stehen – kein leerer Platzhalterrahmen. --}} +
+

{!! $content['title'] ?? '' !!}

+

{{ $content['text'] ?? '' }}

+
+ @endif +
+
diff --git a/resources/views/livewire/web/components/sections/partner-c-t-a.blade.php b/resources/views/livewire/web/components/sections/partner-c-t-a.blade.php index b1b4e867..55049264 100644 --- a/resources/views/livewire/web/components/sections/partner-c-t-a.blade.php +++ b/resources/views/livewire/web/components/sections/partner-c-t-a.blade.php @@ -1,4 +1,4 @@ -
+

diff --git a/resources/views/livewire/web/components/sections/reference-projects.blade.php b/resources/views/livewire/web/components/sections/reference-projects.blade.php new file mode 100644 index 00000000..357addc5 --- /dev/null +++ b/resources/views/livewire/web/components/sections/reference-projects.blade.php @@ -0,0 +1,67 @@ +
+
+
+

{!! $content['title'] ?? '' !!}

+ @if (!empty($content['text'])) +

{{ $content['text'] }}

+ @endif +
+ + @if ($projects) +
+ @foreach ($projects as $project) +
+
+ {{ $project['caption'] ?: ($project['brands'][0]['name'] ?? '') }} + + {{ __('ui.brand_gallery_badge_project') }} + +
+
+ @if (!empty($project['caption'])) +

{{ $project['caption'] }}

+ @endif + @if (!empty($project['meta'])) +

{{ $project['meta'] }}

+ @endif + @if (!empty($project['description'])) +

{{ $project['description'] }}

+ @endif + @if (!empty($project['gallery'])) + {{-- Weitere Bilder der Referenz als kleine Reihe --}} +
+ @foreach ($project['gallery'] as $item) + {{ $item['caption'] ?: $project['caption'] }} + @endforeach +
+ @endif + @if (!empty($project['brands'])) +

+ @foreach ($project['brands'] as $brand) + @if (!empty($brand['link'])) + {{ $brand['name'] }} + @else + {{ $brand['name'] }} + @endif + @endforeach +

+ @endif +
+
+ @endforeach +
+ @else + {{-- Ehrlicher Leerzustand statt einer erfundenen Referenz. --}} +
+

{{ $content['empty'] ?? __('ui.references_empty') }}

+ + {{ __('ui.request_appointment') }} + +
+ @endif +
+
diff --git a/resources/views/livewire/web/components/sections/sponsoring.blade.php b/resources/views/livewire/web/components/sections/sponsoring.blade.php new file mode 100644 index 00000000..13b26f4f --- /dev/null +++ b/resources/views/livewire/web/components/sections/sponsoring.blade.php @@ -0,0 +1,33 @@ +
+
+
+

{!! $content['title'] ?? '' !!}

+ @if (!empty($content['subtitle'])) +

{{ $content['subtitle'] }}

+ @endif +
+ + {{-- Kachel je Verein: Logo oben auf Weiß (die Logos sind für weißen Grund gemacht), + darunter ein kurzer Text. Bewusst ohne Verlinkung – reine Nennung. --}} +
+ @foreach ($content['logos'] ?? [] as $index => $logo) +
+ {{-- Feste Bühnenhöhe und Deckel in beide Richtungen: die Logos haben sehr + unterschiedliche Seitenverhältnisse (1:1 bis 3,3:1) und wirken sonst + unterschiedlich groß. --}} +
+ {{ $logo['name'] ?? '' }} +
+ +
+

{{ $logo['name'] ?? '' }}

+ @if (!empty($logo['text'])) +

{{ $logo['text'] }}

+ @endif +
+
+ @endforeach +
+
+
diff --git a/resources/views/livewire/web/components/sections/vision-section.blade.php b/resources/views/livewire/web/components/sections/vision-section.blade.php index 28b934a3..d16b22d7 100644 --- a/resources/views/livewire/web/components/sections/vision-section.blade.php +++ b/resources/views/livewire/web/components/sections/vision-section.blade.php @@ -1,4 +1,4 @@ -
+
diff --git a/resources/views/livewire/web/components/ui/announcement-bar.blade.php b/resources/views/livewire/web/components/ui/announcement-bar.blade.php index 76774575..26d89f3c 100644 --- a/resources/views/livewire/web/components/ui/announcement-bar.blade.php +++ b/resources/views/livewire/web/components/ui/announcement-bar.blade.php @@ -1,5 +1,16 @@ @if (!empty($content) && ($content['enabled'] ?? false)) -
@@ -28,7 +39,7 @@ @endif +
+ + @if ($success) +
+ {{ $content['success'] ?? __('ui.appointment.success') }} +
+ + @else + @if (!empty($content['intro'])) +

{{ $content['intro'] }}

+ @endif + + {{-- WhatsApp zuerst (Abstimmung 23.09.): der schnellste Weg steht oben, das Formular + darunter. Nur sichtbar, wenn im CMS eine Nummer hinterlegt ist. Der Link ist ein + normaler Klick-zum-Chat-Link – auf dem Handy öffnet die installierte App, sonst + WhatsApp Web. Der Text ist aus dem Formularstand vorbereitet, abgeschickt wird er + vom Besucher selbst. --}} + @if ($this->whatsappUrl !== '') +
+

{{ __('ui.appointment.whatsapp_intro') }}

+ + + {{ __('ui.whatsapp') }} + +

{{ __('ui.appointment.whatsapp_note') }}

+ + +
+ @endif + + +
+ + + @error('name') {{ $message }} @enderror +
+ +
+ + + @error('email') {{ $message }} @enderror +
+ +
+ + + @error('phone') {{ $message }} @enderror +
+ +
+ + + @error('topic') {{ $message }} @enderror +
+ +
+ + + @error('preferredDate') {{ $message }} @enderror +
+ +
+ + + @error('message') {{ $message }} @enderror +
+ + {{-- Honeypot --}} + + + + @error('privacy') {{ $message }} @enderror + + + + + @endif +
+ +
+ +
+
diff --git a/resources/views/livewire/web/components/ui/contact-form.blade.php b/resources/views/livewire/web/components/ui/contact-form.blade.php index 66aa4340..32d20d46 100644 --- a/resources/views/livewire/web/components/ui/contact-form.blade.php +++ b/resources/views/livewire/web/components/ui/contact-form.blade.php @@ -1,6 +1,6 @@
-
+
@@ -40,7 +40,7 @@ wire:model="firstName" id="firstName" type="text" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6" + class="form-field" required /> @error('firstName') {{ $message }} @enderror @@ -53,7 +53,7 @@ wire:model="lastName" id="lastName" type="text" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6" + class="form-field" required /> @error('lastName') {{ $message }} @enderror @@ -68,7 +68,7 @@ wire:model="company" id="company" type="text" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6" + class="form-field" /> @error('company') {{ $message }} @enderror
@@ -82,7 +82,7 @@ wire:model="email" id="email" type="email" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6" + class="form-field" required /> @error('email') {{ $message }} @enderror @@ -95,7 +95,7 @@ wire:model="phone" id="phone" type="tel" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6" + class="form-field" /> @error('phone') {{ $message }} @enderror
@@ -105,18 +105,24 @@ -
+
-
@error('subject') {{ $message }} @enderror
@@ -131,7 +137,7 @@ wire:model="message" id="message" rows="5" - class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6 resize-none" + class="form-field resize-none" placeholder="{{ $content['form']['placeholders']['message'] ?? __('ui.contact_form.message_placeholder') }}" required > diff --git a/resources/views/livewire/web/components/ui/footer.blade.php b/resources/views/livewire/web/components/ui/footer.blade.php index ec3c04f7..fea18eb8 100644 --- a/resources/views/livewire/web/components/ui/footer.blade.php +++ b/resources/views/livewire/web/components/ui/footer.blade.php @@ -1,56 +1,81 @@ -