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>
176 lines
12 KiB
PHP
176 lines
12 KiB
PHP
{{-- 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>
|