markemacht/resources/views/components/web/⚡audit-form.blade.php
Kevin Adametz 1f7c0aa430 feat(marketing): Audit-Anfrageformular als Livewire-Komponente
Das markemacht.de-Anfrageformular (Audit-Modal) war nach der Laravel-
Integration ohne Endpoint. Es wird jetzt serverseitig über Livewire
verarbeitet statt über das alte statische PHP-Skript.

- AuditAnfrage-Mailable + Mail-View, Reply-To = Absender
- config/marketing.php: Empfänger via MARKETING_ANFRAGE_TO (Default info@markemacht.de)
- Livewire-SFC web.audit-form: Validierung (dt.), Honeypot, Rate-Limit (3/10 Min),
  dispatch('audit-sent') schließt das Modal
- Modal-Markup auf <livewire:web.audit-form /> umgestellt
- Feature-Tests (Validierung, Versand, Honeypot, Throttle)
- Entwicklungsplan unter dev/audit-formular-cookies/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:20:21 +00:00

140 lines
5.1 KiB
PHP

<?php
use App\Mail\AuditAnfrage;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
new class extends Component
{
public string $name = '';
public string $email = '';
public string $message = '';
public bool $privacy = false;
/** Honeypot-Felder — für Menschen unsichtbar, von Bots befüllt. */
public string $company = '';
public string $url = '';
/** Statusmeldung für aria-live, sichtbar nach Absenden. */
public string $status = '';
public bool $sent = false;
/**
* @return array<string, string>
*/
protected function rules(): array
{
return [
'name' => 'required|string|max:120',
'email' => 'required|email|max:190',
'message' => 'required|string|max:5000',
'privacy' => 'accepted',
];
}
/**
* @return array<string, string>
*/
protected function messages(): array
{
return [
'name.required' => 'Bitte geben Sie Ihren Namen an.',
'email.required' => 'Bitte geben Sie Ihre E-Mail-Adresse an.',
'email.email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
'message.required' => 'Bitte beschreiben Sie kurz Ihr Anliegen.',
'privacy.accepted' => 'Bitte akzeptieren Sie die Datenschutzerklärung.',
];
}
public function submit(): void
{
// Spam-Schutz: Bots befüllen die versteckten Felder. Wir tun so, als sei alles gut.
if ($this->company !== '' || $this->url !== '') {
$this->markeAlsGesendet('Danke. Ihre Anfrage wurde aufgenommen.');
return;
}
$this->validate();
$key = 'audit-anfrage:'.request()->ip();
if (RateLimiter::tooManyAttempts($key, maxAttempts: 3)) {
$this->status = 'Zu viele Anfragen in kurzer Zeit. Bitte versuchen Sie es später erneut oder schreiben Sie an info@markemacht.de.';
return;
}
RateLimiter::hit($key, decaySeconds: 600);
Mail::to(config('marketing.anfrage_empfaenger'))->send(
new AuditAnfrage($this->name, $this->email, $this->message),
);
$this->markeAlsGesendet('Danke. Ihre Anfrage wurde versendet.');
}
private function markeAlsGesendet(string $meldung): void
{
$this->reset(['name', 'email', 'message', 'privacy', 'company', 'url']);
$this->status = $meldung;
$this->sent = true;
// markemacht.js lauscht auf dieses Event und schließt das Modal nach kurzer Zeit.
$this->dispatch('audit-sent');
}
};
?>
<div>
@if ($sent)
<p class="form-status body-text" aria-live="polite">{{ $status }}</p>
@else
<form class="modal-form stack-16" wire:submit="submit" novalidate>
{{-- Honeypot: außerhalb des sichtbaren Flusses, für Bots --}}
<div aria-hidden="true" tabindex="-1" style="position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;">
<label>Firma<input type="text" tabindex="-1" autocomplete="off" wire:model="company" /></label>
<label>URL<input type="text" tabindex="-1" autocomplete="off" wire:model="url" /></label>
</div>
<div class="form-field">
<label class="meta" for="audit-name">Name</label>
<input id="audit-name" type="text" autocomplete="name" wire:model="name" />
@error('name') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<div class="form-field">
<label class="meta" for="audit-email">E-Mail</label>
<input id="audit-email" type="email" autocomplete="email" wire:model="email" />
@error('email') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<div class="form-field">
<label class="meta" for="audit-message">Worum geht es?</label>
<textarea id="audit-message" rows="5" wire:model="message"></textarea>
@error('message') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<label class="form-check">
<input type="checkbox" wire:model="privacy" />
<span class="form-switch" aria-hidden="true"></span>
<span>Ich akzeptiere die <a href="{{ route('datenschutz') }}">Datenschutzerklärung</a>.</span>
</label>
@error('privacy') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
<p class="form-status body-text" aria-live="polite">{{ $status }}</p>
<div>
<button class="btn btn-nav-cta" type="submit" wire:loading.attr="disabled" wire:target="submit">
<span wire:loading.remove wire:target="submit">Absenden</span>
<span wire:loading wire:target="submit">Senden …</span>
<svg class="icon" aria-hidden="true"><use href="/web/markemacht/icons.svg#arrow-forward"></use></svg>
</button>
</div>
</form>
@endif
</div>