12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,331 @@
<?php
use App\Models\PressRelease;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component
{
#[Locked]
public int $id;
public string $rejectReason = '';
public function mount(int $id): void
{
$this->id = $id;
}
public function publish(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
try {
app(PressReleaseService::class)->publish($pr);
} catch (BlacklistViolationException $e) {
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
Flux::modal('confirm-show-publish')->close();
return;
}
session()->flash('success', __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'));
Flux::modal('confirm-show-publish')->close();
}
public function reject(): void
{
$this->validate([
'rejectReason' => ['required', 'string', 'min:5', 'max:2000'],
]);
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->reject($pr, trim($this->rejectReason));
$this->rejectReason = '';
session()->flash('success', __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'));
Flux::modal('confirm-show-reject')->close();
}
public function archive(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->archive($pr);
session()->flash('success', __('Archiviert.'));
Flux::modal('confirm-show-archive')->close();
}
public function with(): array
{
$pr = PressRelease::withoutGlobalScopes()
->with([
'company:id,name,slug',
'category.translations',
'user:id,name',
'images',
'statusLogs.changedBy:id,name',
])
->findOrFail($this->id);
return [
'pr' => $pr,
'statusLogs' => $pr->statusLogs,
'categoryName' => $pr->category?->translations->firstWhere('locale', 'de')?->name
?? $pr->category?->translations->first()?->name
?? '',
'statusColor' => match ($pr->status->value) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
},
];
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
{{ session('success') }}
</div>
@endif
<flux:card>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="flex-1">
<div class="flex items-center gap-2">
<flux:badge :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ strtoupper($pr->language) }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ $pr->portal->label() }}</flux:badge>
</div>
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Firma') }}: {{ $pr->company?->name ?? '' }} ·
{{ __('Kategorie') }}: {{ $categoryName }} ·
{{ __('Autor') }}: {{ $pr->user?->name ?? '' }}
</flux:text>
</div>
<div class="flex gap-2">
<flux:button variant="ghost" icon="pencil" href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</div>
</flux:card>
{{-- Status-Aktionen --}}
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
<flux:card>
<div class="flex flex-wrap items-center gap-3">
<flux:text weight="medium" class="text-yellow-700 dark:text-yellow-400">
{{ __('Diese PM wartet auf Prüfung.') }}
</flux:text>
<flux:modal.trigger name="confirm-show-publish">
<flux:button type="button" variant="primary">{{ __('Veröffentlichen') }}</flux:button>
</flux:modal.trigger>
<flux:modal.trigger name="confirm-show-reject">
<flux:button type="button" variant="danger">{{ __('Ablehnen') }}</flux:button>
</flux:modal.trigger>
</div>
</flux:card>
@endif
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
<flux:card>
<div class="flex items-center gap-3">
<flux:modal.trigger name="confirm-show-archive">
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
</flux:modal.trigger>
@if($pr->hits > 0)
<flux:text class="text-sm text-zinc-500">{{ number_format($pr->hits) }} {{ __('Aufrufe') }}</flux:text>
@endif
</div>
</flux:card>
@endif
<div class="grid gap-6 lg:grid-cols-[1fr,280px]">
{{-- Text --}}
<flux:card>
<div class="prose prose-zinc dark:prose-invert max-w-none">
{!! nl2br(e($pr->text)) !!}
</div>
</flux:card>
{{-- Details --}}
<div class="space-y-4">
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Details') }}</flux:heading>
<dl class="space-y-2 text-sm">
<div class="flex justify-between gap-2">
<dt class="text-zinc-500">{{ __('Status') }}</dt>
<dd class="font-medium">{{ $pr->status->label() }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-zinc-500">{{ __('Erstellt') }}</dt>
<dd>{{ $pr->created_at->format('d.m.Y H:i') }}</dd>
</div>
@if($pr->published_at)
<div class="flex justify-between gap-2">
<dt class="text-zinc-500">{{ __('Veröffentlicht') }}</dt>
<dd>{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
</div>
@endif
<div class="flex justify-between gap-2">
<dt class="text-zinc-500">{{ __('Aufrufe') }}</dt>
<dd>{{ number_format($pr->hits) }}</dd>
</div>
@if($pr->keywords)
<div>
<dt class="text-zinc-500">{{ __('Stichwörter') }}</dt>
<dd class="mt-1">{{ $pr->keywords }}</dd>
</div>
@endif
@if($pr->backlink_url)
<div>
<dt class="text-zinc-500">{{ __('Backlink') }}</dt>
<dd class="mt-1 break-all">
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline dark:text-blue-400">
{{ $pr->backlink_url }}
</a>
</dd>
</div>
@endif
@if($pr->no_export)
<div class="rounded bg-zinc-100 px-2 py-1 text-xs text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">
{{ __('Kein Export') }}
</div>
@endif
</dl>
</flux:card>
@if($pr->images->isNotEmpty())
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Bilder') }}</flux:heading>
<div class="space-y-2">
@foreach($pr->images as $image)
<div class="flex items-center gap-2 text-sm">
<flux:icon.photo class="size-4 text-zinc-400" />
<span class="truncate text-zinc-600 dark:text-zinc-400">{{ basename($image->path) }}</span>
@if($image->is_preview)
<flux:badge size="sm" color="blue">{{ __('Preview') }}</flux:badge>
@endif
</div>
@endforeach
</div>
</flux:card>
@endif
</div>
</div>
@if($statusLogs->isNotEmpty())
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Status-Verlauf') }}</flux:heading>
<ol class="space-y-3 border-s border-zinc-200 ps-4 dark:border-zinc-700">
@foreach($statusLogs as $log)
<li class="text-sm">
<div class="flex flex-wrap items-center gap-2">
@php
$color = match($log->to_status?->value) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
};
@endphp
<flux:badge size="sm" :color="$color">{{ $log->to_status?->label() ?? $log->to_status }}</flux:badge>
@if($log->from_status)
<span class="text-xs text-zinc-500">
{{ __('von') }} {{ $log->from_status->label() }}
</span>
@endif
<span class="text-xs text-zinc-500">·</span>
<span class="text-xs text-zinc-500">
{{ $log->created_at->format('d.m.Y H:i') }}
</span>
@if($log->changedBy)
<span class="text-xs text-zinc-500">·</span>
<span class="text-xs text-zinc-500">{{ $log->changedBy->name }}</span>
@endif
@if($log->source !== 'admin')
<flux:badge size="xs" color="zinc">{{ $log->source }}</flux:badge>
@endif
</div>
@if($log->reason)
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
@endif
</li>
@endforeach
</ol>
</flux:card>
@endif
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
<flux:modal name="confirm-show-publish" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung veröffentlichen?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung wird öffentlich sichtbar und der Autor wird benachrichtigt.') }}</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="publish">{{ __('Veröffentlichen') }}</flux:button>
</div>
</div>
</flux:modal>
<flux:modal name="confirm-show-reject" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung ablehnen?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung wird abgelehnt und der Autor wird benachrichtigt. Bitte begründen Sie die Ablehnung.') }}</flux:subheading>
</div>
<flux:field>
<flux:label>{{ __('Begründung (an den Autor sichtbar)') }}</flux:label>
<flux:textarea
wire:model="rejectReason"
rows="5"
placeholder="{{ __('z. B. Werbliche Sprache, fehlende Belege, doppelte Veröffentlichung…') }}"
/>
<flux:error name="rejectReason" />
</flux:field>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" wire:click="reject">{{ __('Ablehnen') }}</flux:button>
</div>
</div>
</flux:modal>
@endif
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
<flux:modal name="confirm-show-archive" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung archivieren?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung bleibt intern erhalten, wird aber archiviert.') }}</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="archive">{{ __('Archivieren') }}</flux:button>
</div>
</div>
</flux:modal>
@endif
</div>