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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:00:15 +00:00

261 lines
8.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\Setting;
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\CmsContentService;
if (! function_exists('legal_page')) {
/**
* Rechtstexte: zuerst CMS (Gruppe „legal“), sonst Lang-Datei b2in_legal.
*
* @return array<string, mixed>
*/
function legal_page(string $key): array
{
$fromCms = app(CmsContentService::class)->get('legal.'.$key);
if (is_array($fromCms) && isset($fromCms['content'])) {
return $fromCms;
}
return trans('b2in_legal.'.$key);
}
}
if (! function_exists('cms')) {
/**
* Resolve a CMS content value by dot-notation key.
*
* First segment is the group, rest is the key: "home.hero.title"
* Falls back to __() if no DB entry exists.
*
* @param array<string, string> $replace
*/
function cms(string $key, array $replace = [], ?string $locale = null): mixed
{
return app(CmsContentService::class)->get($key, $replace, $locale);
}
}
if (! function_exists('cms_theme_section')) {
/**
* Theme-Sektion: zuerst CMS (gemäß cms_section_map), sonst Lang b2in.themes.{theme}.{section}.
*
* @return array<string, mixed>|string|mixed
*/
function cms_theme_section(string $sectionKey, ?string $theme = null): mixed
{
$theme = $theme ?? config('app.theme', 'b2in');
/** @var array<string, array{0: string, 1: string}> $sections */
$sections = config('cms_section_map.sections', []);
if (isset($sections[$sectionKey])) {
[$group, $key] = $sections[$sectionKey];
$fromCms = app(CmsContentService::class)->getIfExists("{$group}.{$key}");
if ($fromCms !== null) {
return $fromCms;
}
}
return trans("b2in.themes.{$theme}.{$sectionKey}");
}
}
if (! function_exists('references_page_enabled')) {
/**
* Ob die Seite /referenzen samt Menüpunkt sichtbar ist – Schalter im CMS-Modul „Referenzen“
* (Einstellung pages.referenzen_enabled). Ohne Eintrag ist die Seite sichtbar.
*/
function references_page_enabled(): bool
{
return (bool) Setting::getValue('pages', 'referenzen_enabled', true);
}
}
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),
* sonst statische Assets unter public/img/assets/ (Legacy-Pfade wie b2in/…).
*/
function theme_image_url(?string $value): string
{
if ($value === null) {
return '';
}
$trimmed = ltrim(trim($value), '/');
if ($trimmed === '') {
return '';
}
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
return $trimmed;
}
if (str_starts_with($trimmed, 'storage/')) {
return asset($trimmed);
}
if (str_starts_with($trimmed, 'immobile/')) {
return asset('storage/'.$trimmed);
}
// Liegt die Datei unter public/img/assets/, kommt sie von dort – auch wenn sie inzwischen in
// der Mediathek steht (AssetMediaImportSeeder). Nur so findet <x-web-picture> die WebP-Fassung
// neben JPG/PNG.
if (str_contains($trimmed, '/') && is_file(public_path('img/assets/'.$trimmed))) {
return asset('img/assets/'.$trimmed);
}
$candidates = array_values(array_unique(array_filter([$trimmed, basename($trimmed)])));
foreach ($candidates as $candidate) {
if (CmsMedia::query()->where('filename', $candidate)->exists()) {
return media_url($candidate);
}
}
if (str_contains($trimmed, '/')) {
return asset('img/assets/'.$trimmed);
}
return media_url($trimmed);
}
}
if (! function_exists('tcms')) {
/**
* Typed CMS – always returns a string.
*
* @param array<string, string> $replace
*/
function tcms(string $key, array $replace = [], ?string $locale = null): string
{
$text = cms($key, $replace, $locale);
return is_string($text) ? $text : (string) $text;
}
}
if (! function_exists('media_url')) {
/**
* Resolve a CmsMedia filename to its full storage URL.
* Cached per request to avoid repeated DB queries.
*
* Ohne Eintrag in der Mediathek zeigt die URL auf public/img/assets/ – dort legen die Seeder
* Marken-, Magazin- und Slideshow-Bilder ab (wie theme_image_url() im Frontend). Der frühere
* Rückfall assets/images/ existierte nicht und lieferte im Admin 404 (07.10.2026).
*/
function media_url(?string $filename, string $profile = ''): string
{
if (! $filename || $filename === '') {
return '';
}
// once(): je Request (und je Test) zwischengespeichert, nicht über die ganze Prozesslaufzeit.
return once(function () use ($filename, $profile): string {
$media = CmsMedia::where('filename', $filename)->first();
if (! $media) {
return asset('img/assets/'.ltrim($filename, '/'));
}
return $profile && $media->hasConversion($profile)
? $media->getConversionUrl($profile)
: $media->getUrl();
});
}
}
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_worlds')) {
/**
* Die beiden Welten für den Welten-Wechsler in Kopf- und Fußzeile.
*
* @return array<string, array{label: string, url: string, active: bool}>
*/
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);
}
}