Arbeitsplan 14.09.2026, Punkte 1.1 bis 1.4 (Nachträge 6 bis 10 im Entwicklungsplan): - Farbwelt Interior (Tanne, Creme, Sand, Ocker, Clay) als Standard, Immobilien-Welt behält das bisherige Blau über body.world-immobilien; Logo je Welt, Favicons neu, Welten-Wechsler in Kopf- und Fußzeile. - Saubere Trennung der Welten: eigene Navigation, Ankündigungsleiste, Magazinliste, Über-uns-Seite, FAQ, Kontaktseite und Abschluss-CTA für Immobilien; Beiträge tragen eine Welt (Migration + Admin-Feld). Abschluss-CTA Ton in Ton statt Grün-zu-Ocker. - Kontakt: Betreffe in Gruppen Interior / Immobilien / B2in, Marken darin. Terminanfrage-Buttons öffnen das Widget mit vorgewähltem Anliegen; Select-Felder einheitlich gestaltet. - Tischlerei als vierte Kernmarke (Platzhalter), Kachelraster 2×2. - Herkunfts-Sektion „Aus der Region gefertigt“ mit schematischer Standortkarte und Ablauf CABINET (Beratung Bielefeld, Fertigung Köln, Aufbau mit Tischlerei). Deployment: migrate, WorldSplitCmsSeeder, CmsBrandSeeder, OriginSectionCmsSeeder (Abschnitt 8 im Entwicklungsplan). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
166 lines
5.1 KiB
PHP
166 lines
5.1 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Web\Components\Ui;
|
||
|
||
use Acme\ContactForm\ContactFormService;
|
||
use Acme\ContactForm\SpamDetector;
|
||
use App\Models\CmsBrand;
|
||
use Livewire\Component;
|
||
|
||
/**
|
||
* Schwebende Terminanfrage – auf allen Seiten unten rechts.
|
||
*
|
||
* Setzt wie die übrigen Formulare der Seite auf {@see ContactFormService} und {@see SpamDetector}
|
||
* auf, statt auf Acme\ContactForm\Livewire\ContactForm: dessen mitgelieferte View bringt ein
|
||
* eigenes (blaues) Layout mit, das nicht zum Auftritt passt. Honeypot, Rate-Limit und
|
||
* Spam-Erkennung kommen so trotzdem aus dem Paket.
|
||
*/
|
||
class AppointmentWidget extends Component
|
||
{
|
||
/** Feste Anliegen neben den gepflegten Marken. */
|
||
public const EXTRA_TOPICS = [
|
||
'projektausstattung' => '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<string, mixed> */
|
||
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<string, string>
|
||
*/
|
||
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');
|
||
}
|
||
}
|