b2in/resources/views/livewire/web/components/ui/appointment-widget.blade.php
Kevin Adametz 4ef3a4d8f1 Markenseiten Rotpunkt und Febrü aus der Zulieferung vom 31.08.
Umsetzung der beiden finalen Webseitentexte aus dev/2026-08-28/.
Die angekündigte Aufgabenliste umsetzung-03-09-2026.md war leer, Grundlage
sind daher die Umsetzungshinweise in den PDFs selbst.

- Neue Felder in cms_brands: sections, widget_label, cta_title, cta_text.
  Einleitung und Leistungskacheln reichten für diese Texte nicht aus.
- Die als Vertiefung gelieferten Blöcke werden aufklappbar dargestellt,
  mit aria-expanded und aria-controls. Bewusst x-transition statt
  x-collapse, weil das Collapse-Plugin hier nicht registriert ist.
- Kein zweiter CTA-Button: das schwebende Terminanfrage-Widget trägt auf
  der Markenseite die Beschriftung der Marke und liest den Kontext aus
  der Route, da es im Layout und nicht je Seite eingebunden ist.
- Footer bekommt unten Platz und lange Beschriftungen brechen um, damit
  das Widget auf dem Smartphone nichts verdeckt.
- Abschnitte sind im Admin vollständig pflegbar, inklusive Sortierung.
- Interne Quellenhinweise der PDFs sind nicht übernommen; ein Test prüft,
  dass sie nicht im Ausgabe-HTML stehen.
- BrandPageTextSeeder fasst nur die Textfelder an, Bilder, Galerie und
  Leistungen bleiben unangetastet.

Vor der Veröffentlichung: Herstellerfreigabe einholen und die Aussage zur
erhaltenen Waldfläche nur mit ausdrücklicher Bestätigung verwenden. Beides
steht in der Deployment-Checkliste.

854 Tests grün (3150 Assertions), Pint sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:52:36 +00:00

176 lines
12 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.

{{-- Schwebende Terminanfrage, unten rechts. Der Cookie-Button des Consent-Managers sitzt unten
links (z-40), sein Dialog auf z-50 – dieses Widget bleibt darunter, damit die Einwilligung
immer erreichbar bleibt. --}}
<div class="fixed bottom-4 right-4 z-40 flex flex-col items-end gap-3 print:hidden"
x-data="{
open: false,
whatsappBase: @js($this->whatsappUrl),
topicLabels: @js($this->topics),
greeting: @js($content['whatsapp_text'] ?? __('ui.appointment.whatsapp_text')),
/* Baut den Nachrichtentext aus dem, was schon im Formular steht. Der Besucher sieht ihn
in WhatsApp und schickt ihn selbst ab – wir verschicken nichts automatisch. */
get whatsappHref() {
const lines = [this.greeting];
const topic = this.topicLabels[$wire.topic];
if (topic) { lines.push('Anliegen: ' + topic) }
if ($wire.preferredDate) { lines.push('Wunschtermin: ' + $wire.preferredDate) }
if ($wire.name) { lines.push('Mein Name: ' + $wire.name) }
return this.whatsappBase + '?text=' + encodeURIComponent(lines.join('\n'));
},
}" x-on:keydown.escape.window="open = false">
{{-- Panel --}}
<div x-show="open" x-cloak x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-2" x-transition:leave="transition ease-in duration-150"
x-transition:leave-end="opacity-0 translate-y-2" x-on:click.outside="open = false"
role="dialog" aria-modal="true" aria-labelledby="appointment-widget-title"
class="card-elevated w-[calc(100vw-2rem)] sm:w-[26rem] max-h-[80vh] overflow-y-auto rounded-2xl p-6">
<div class="flex items-start justify-between gap-4">
<h2 id="appointment-widget-title" class="text-xl font-medium">
{{ $content['title'] ?? __('ui.request_appointment') }}
</h2>
<button type="button" x-on:click="open = false" aria-label="{{ __('ui.close') }}"
class="shrink-0 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@if ($success)
<div class="mt-6 rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-800">
{{ $content['success'] ?? __('ui.appointment.success') }}
</div>
<button type="button" wire:click="startOver"
class="btn-secondary-accent w-full mt-4 px-6 py-3 cursor-pointer">
{{ __('ui.appointment.new_request') }}
</button>
@else
@if (!empty($content['intro']))
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $content['intro'] }}</p>
@endif
<form wire:submit="submit" class="space-y-4 mt-6">
<div>
<label for="appointment-name" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.name') }} <span aria-hidden="true">*</span>
</label>
<input wire:model="name" id="appointment-name" type="text" required
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" />
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="appointment-email" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.email') }} <span aria-hidden="true">*</span>
</label>
<input wire:model="email" id="appointment-email" type="email" required
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" />
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="appointment-phone" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.phone') }}
</label>
<input wire:model="phone" id="appointment-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" />
@error('phone') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="appointment-topic" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.topic') }}
</label>
<select wire:model="topic" id="appointment-topic"
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">
<option value="">{{ __('ui.appointment.topic_placeholder') }}</option>
@foreach ($this->topics as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
@error('topic') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="appointment-date" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.preferred_date') }}
</label>
<input wire:model="preferredDate" id="appointment-date" type="date"
min="{{ now()->toDateString() }}"
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" />
@error('preferredDate') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="appointment-message" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.message') }}
</label>
<textarea wire:model="message" id="appointment-message" rows="3"
placeholder="{{ __('ui.appointment.message_placeholder') }}"
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"></textarea>
@error('message') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
{{-- Honeypot --}}
<div class="opacity-0 absolute top-0 left-0 h-0 w-0 -z-10 overflow-hidden" aria-hidden="true">
<label for="appointment-website">Website</label>
<input type="text" id="appointment-website" wire:model="website" tabindex="-1" autocomplete="off" />
</div>
<label class="flex items-start gap-3 cursor-pointer">
<input wire:model="privacy" id="appointment-privacy" type="checkbox"
class="mt-1 shrink-0 rounded border-border" />
<span class="text-sm text-muted-foreground">
{{ __('ui.contact_form.privacy_prefix') }}
<a href="{{ route('privacy') }}" target="_blank"
class="text-secondary underline underline-offset-2 hover:no-underline">Datenschutzerklärung</a>
{{ __('ui.contact_form.privacy_suffix') }}
</span>
</label>
@error('privacy') <span class="text-red-500 text-xs block">{{ $message }}</span> @enderror
<button type="submit" wire:loading.attr="disabled"
class="btn-primary-accent w-full px-6 py-3 cursor-pointer disabled:opacity-70">
<span wire:loading.remove wire:target="submit">{{ __('ui.appointment.submit') }}</span>
<span wire:loading wire:target="submit">{{ __('ui.appointment.sending') }}</span>
</button>
</form>
{{-- Alternative: WhatsApp. 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 vorbereitet, abgeschickt
wird er vom Besucher selbst. --}}
@if ($this->whatsappUrl !== '')
<div class="mt-6 pt-6 border-t border-border/60">
<p class="text-sm text-muted-foreground">{{ __('ui.appointment.whatsapp_intro') }}</p>
<a x-bind:href="whatsappHref" target="_blank" rel="noopener"
class="mt-3 flex w-full items-center justify-center gap-2 rounded-lg bg-[#25D366] px-6 py-3 font-medium text-white transition-transform hover:scale-[1.02]">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.247-.694.247-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884a9.82 9.82 0 016.988 2.896 9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.885-9.885 9.885M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.359.101 11.945c0 2.096.549 4.142 1.595 5.945L0 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.582 0 11.941-5.359 11.944-11.945a11.87 11.87 0 00-3.417-8.4" />
</svg>
{{ __('ui.whatsapp') }}
</a>
<p class="mt-2 text-xs text-muted-foreground">{{ __('ui.appointment.whatsapp_note') }}</p>
</div>
@endif
@endif
</div>
<div class="flex items-center gap-3">
<button type="button" x-on:click="open = !open" x-bind:aria-expanded="open.toString()"
aria-controls="appointment-widget-title"
class="btn-secondary-accent flex items-center gap-2 rounded-full px-5 py-3 shadow-lg cursor-pointer
max-w-[calc(100vw-2rem)] text-left leading-tight">
<svg class="h-5 w-5 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
<span x-show="!open">{{ $content['button_label'] ?? __('ui.request_appointment') }}</span>
<span x-show="open" x-cloak>{{ __('ui.close') }}</span>
</button>
</div>
</div>