images: Titelbild-Formular v2, Admin-Upload mit Kundenbestätigung, Entfernen mit Sofortlöschung (Bildkennzeichnung Block B)
- press-release-images-manager-v2 hinter IMAGE_RIGHTS_V2: drei Blöcke, Vorbelegung aus Metadaten, Bestätigung mit archiviertem Wortlaut, Bild-Bindung über Hash, Fokuspunkt und Letterbox, Zustände absent/prefilled/error - CustomerImageConfirmation: Magic-Link an den Kontoinhaber, Karte auf der Kundenseite, Kanal magic_link - PressReleaseImageRemover: nie veröffentlicht = alles sofort weg, veröffentlicht = Soft Delete, Original bleibt - ConversionSource::MailImageConfirmation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
214aea8910
commit
3d3ddc2dc8
11 changed files with 1380 additions and 5 deletions
|
|
@ -0,0 +1,35 @@
|
|||
<x-emails.layout title="Bitte bestätigen Sie das Titelbild" preheader="Für Ihre Pressemitteilung wurde ein Titelbild hinterlegt. Es wird erst veröffentlicht, wenn Sie die Bildrechte bestätigen.">
|
||||
<p>{{ filled($recipientName) ? 'Hallo '.$recipientName.',' : 'Guten Tag,' }}</p>
|
||||
|
||||
<p>
|
||||
für Ihre Pressemitteilung wurde von unserem Team ein Titelbild hinterlegt.
|
||||
Damit es veröffentlicht werden kann, brauchen wir Ihre Bestätigung, dass Sie
|
||||
zur Veröffentlichung des Bildes berechtigt sind und die Angaben dazu stimmen.
|
||||
</p>
|
||||
|
||||
<p class="title">{{ $pressRelease->title }}</p>
|
||||
|
||||
<dl class="meta">
|
||||
<dt>Bildnachweis</dt>
|
||||
<dd>{{ $image->copyright ?: '—' }}</dd>
|
||||
<dt>KI-Kennzeichnung</dt>
|
||||
<dd>{{ $image->ai_declaration?->label() ?? 'noch offen' }}</dd>
|
||||
</dl>
|
||||
|
||||
<x-emails.button :url="$confirmationUrl">Bild ansehen und bestätigen</x-emails.button>
|
||||
|
||||
<p>
|
||||
Der Link meldet Sie direkt in Ihrem Konto an und ist gültig bis
|
||||
<strong>{{ $expiresAt->format('d.m.Y H:i') }} Uhr</strong>. Danach können Sie
|
||||
sich jederzeit regulär anmelden und die Bestätigung auf der Seite der
|
||||
Pressemitteilung nachholen.
|
||||
</p>
|
||||
|
||||
<div class="note">
|
||||
<p>
|
||||
Ohne Ihre Bestätigung bleibt die Pressemitteilung mit dem Platzhalter-Bild.
|
||||
Wenn das Bild nicht von Ihnen stammt oder die Angaben nicht stimmen,
|
||||
antworten Sie bitte auf diese E-Mail.
|
||||
</p>
|
||||
</div>
|
||||
</x-emails.layout>
|
||||
|
|
@ -691,7 +691,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
</section>
|
||||
|
||||
{{-- 5) MEDIEN --}}
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
@if (config('images.rights_v2'))
|
||||
<livewire:components.press-release-images-manager-v2 :press-release-id="$id" :wire:key="'pr-images-v2-'.$id" />
|
||||
@else
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
@endif
|
||||
|
||||
{{-- 6) ANHÄNGE — TEMPORÄR DEAKTIVIERT
|
||||
Datei-Uploads erfordern eine vollständige Sicherheitsprüfung
|
||||
|
|
|
|||
|
|
@ -0,0 +1,738 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\AiImageDeclaration;
|
||||
use App\Enums\ImageProvenanceStatus;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\Image\ImageRightsTextRenderer;
|
||||
use App\Services\Image\ImageService;
|
||||
use App\Services\PressRelease\Images\CustomerImageConfirmation;
|
||||
use App\Services\PressRelease\Images\ImageSubmission;
|
||||
use App\Services\PressRelease\Images\ImageUploadActor;
|
||||
use App\Services\PressRelease\Images\ImageUploadException;
|
||||
use App\Services\PressRelease\Images\PressReleaseImageRemover;
|
||||
use App\Services\PressRelease\Images\PressReleaseImageUploader;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
/**
|
||||
* Titelbild-Manager, Formular v2 (Bildkennzeichnung Block B, Decision-Update
|
||||
* 22.09.2026). Drei Blöcke: Bild, Angaben (Nachweis + KI-Kennzeichnung),
|
||||
* Bestätigung. Herkunftsdaten der Datei belegen vor, sperren aber nichts
|
||||
* (Phase 1). Der Uploader bestimmt alle effektiven Werte serverseitig; die
|
||||
* hier gesperrten Properties sind nur der Merker für die Datei-Bindung (B.2).
|
||||
*
|
||||
* Admin-Upload: keine Bestätigung durch den Admin, das Bild wartet auf die
|
||||
* Kundenbestätigung per Magic-Link (E10).
|
||||
*/
|
||||
new class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
#[Locked]
|
||||
public int $pressReleaseId;
|
||||
|
||||
public $newImage = null;
|
||||
|
||||
public string $newCopyright = '';
|
||||
|
||||
public string $newAiDeclaration = '';
|
||||
|
||||
public string $newTitle = '';
|
||||
|
||||
public string $newDescription = '';
|
||||
|
||||
public string $newLicenseUrl = '';
|
||||
|
||||
public string $newSourceUrl = '';
|
||||
|
||||
public bool $newRightsConfirmed = false;
|
||||
|
||||
public bool $noCrop = false;
|
||||
|
||||
/** Fokuspunkt als "x,y" in Prozent (Neun-Punkte-Wahl, H.3). */
|
||||
public string $focus = '50,50';
|
||||
|
||||
public bool $isUploadFormOpen = false;
|
||||
|
||||
public bool $showLegalDetails = false;
|
||||
|
||||
/** Zuletzt automatisch vorgeschlagener Nachweis (manuelle Eingaben bleiben). */
|
||||
public string $autoCopyright = '';
|
||||
|
||||
#[Locked]
|
||||
public ?string $provenanceStatus = null;
|
||||
|
||||
#[Locked]
|
||||
public ?string $provenanceSha = null;
|
||||
|
||||
#[Locked]
|
||||
public ?string $creditSuggestion = null;
|
||||
|
||||
#[Locked]
|
||||
public ?string $aiSuggestion = null;
|
||||
|
||||
#[Locked]
|
||||
public bool $enhancementHint = false;
|
||||
|
||||
public function mount(int $pressReleaseId): void
|
||||
{
|
||||
$this->pressReleaseId = $pressReleaseId;
|
||||
}
|
||||
|
||||
public function openUploadForm(): void
|
||||
{
|
||||
$this->isUploadFormOpen = true;
|
||||
}
|
||||
|
||||
public function updatedIsUploadFormOpen(bool $isOpen): void
|
||||
{
|
||||
if (!$isOpen) {
|
||||
$this->resetUploadForm();
|
||||
$this->resetErrorBag();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach der Dateiauswahl: Grenzen prüfen, Herkunft lesen, Formular
|
||||
* vorbelegen. Ein Dateiwechsel setzt Vorbelegung und Schalter zurück (B.2).
|
||||
*/
|
||||
public function updatedNewImage(PressReleaseImageUploader $uploader): void
|
||||
{
|
||||
$this->resetProvenance();
|
||||
$this->newRightsConfirmed = false;
|
||||
$this->resetErrorBag();
|
||||
|
||||
if (!$this->newImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validateOnly('newImage', ['newImage' => $this->imageRules()]);
|
||||
|
||||
try {
|
||||
$result = $uploader->preflight($this->newImage);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$this->addError('newImage', $exception->getMessage());
|
||||
$this->reset('newImage');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->provenanceStatus = $result->status->value;
|
||||
$this->provenanceSha = $result->fileSha256;
|
||||
$this->creditSuggestion = $result->creditSuggestion();
|
||||
$this->aiSuggestion = $result->suggestedAiDeclaration()?->value;
|
||||
$this->enhancementHint = (bool) $result->derived['enhancement_hint'];
|
||||
|
||||
// Nachweis vorbelegen: aus den Metadaten, sonst Firmenname — nur,
|
||||
// solange der Nutzer nichts Eigenes eingetragen hat.
|
||||
$suggested = $this->creditSuggestion ?? $this->companyName();
|
||||
|
||||
if ($this->newCopyright === '' || $this->newCopyright === $this->autoCopyright) {
|
||||
$this->newCopyright = $suggested;
|
||||
$this->autoCopyright = $suggested;
|
||||
}
|
||||
|
||||
if ($this->newAiDeclaration === '' && $this->aiSuggestion !== null) {
|
||||
$this->newAiDeclaration = $this->aiSuggestion;
|
||||
}
|
||||
}
|
||||
|
||||
public function closeUploadForm(): void
|
||||
{
|
||||
$this->resetUploadForm();
|
||||
$this->resetErrorBag();
|
||||
|
||||
$this->isUploadFormOpen = false;
|
||||
}
|
||||
|
||||
public function removeNewImage(): void
|
||||
{
|
||||
$this->reset('newImage');
|
||||
$this->resetProvenance();
|
||||
$this->newRightsConfirmed = false;
|
||||
$this->resetErrorBag('newImage');
|
||||
}
|
||||
|
||||
public function newImagePreviewUrl(): ?string
|
||||
{
|
||||
if (!is_object($this->newImage) || !method_exists($this->newImage, 'temporaryUrl')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->newImage->temporaryUrl();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveImage(PressReleaseImageUploader $uploader, ImageRightsTextRenderer $texts, CustomerImageConfirmation $customerConfirmation): void
|
||||
{
|
||||
$pressRelease = $this->getPressRelease();
|
||||
$this->authorize('update', $pressRelease);
|
||||
|
||||
if (!$this->canChangeImages($pressRelease)) {
|
||||
$this->addError('newImage', __('Das Titelbild kann nur bei Entwürfen oder abgelehnten PMs geändert werden.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->titleImageFor($pressRelease) !== null) {
|
||||
$this->addError('newImage', __('Bitte löschen Sie zuerst das vorhandene Titelbild.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isAdmin = $this->actsAsAdmin();
|
||||
|
||||
$this->validate(
|
||||
[
|
||||
'newImage' => $this->imageRules(),
|
||||
'newCopyright' => ['required', 'string', 'max:255'],
|
||||
'newAiDeclaration' => ['required', Rule::enum(AiImageDeclaration::class)],
|
||||
'newTitle' => ['nullable', 'string', 'max:120'],
|
||||
'newDescription' => ['nullable', 'string', 'max:500'],
|
||||
'newLicenseUrl' => ['nullable', 'url:http,https', 'max:2048'],
|
||||
'newSourceUrl' => ['nullable', 'url:http,https', 'max:2048'],
|
||||
'newRightsConfirmed' => [$isAdmin ? 'nullable' : 'accepted'],
|
||||
'focus' => ['required', Rule::in(array_keys($this->focusOptions()))],
|
||||
],
|
||||
[
|
||||
'newCopyright.required' => __('Bitte geben Sie an, wer das Bild erstellt hat oder die Rechte hält.'),
|
||||
'newAiDeclaration.required' => __('Bitte wählen Sie aus, ob das Bild mit KI erstellt oder bearbeitet wurde.'),
|
||||
'newRightsConfirmed.accepted' => __('Die Bestätigung ist erforderlich, um das Bild zu veröffentlichen.'),
|
||||
],
|
||||
);
|
||||
|
||||
$legalText = $texts->current();
|
||||
[$focusX, $focusY] = array_map('intval', explode(',', $this->focus));
|
||||
|
||||
$submission = new ImageSubmission(
|
||||
copyright: trim($this->newCopyright),
|
||||
aiDeclaration: AiImageDeclaration::from($this->newAiDeclaration),
|
||||
rightsConfirmed: $this->newRightsConfirmed,
|
||||
legalTextKey: $legalText?->key,
|
||||
title: $this->newTitle ?: null,
|
||||
description: $this->newDescription ?: null,
|
||||
licenseUrl: $this->newLicenseUrl ?: null,
|
||||
sourceUrl: $this->newSourceUrl ?: null,
|
||||
focus: ['x' => $focusX, 'y' => $focusY],
|
||||
noCrop: $this->noCrop,
|
||||
creditFromSuggestion: $this->creditSuggestion !== null && trim($this->newCopyright) === $this->creditSuggestion,
|
||||
);
|
||||
|
||||
$user = auth()->user();
|
||||
$actor = $isAdmin
|
||||
? ImageUploadActor::admin($user, request()->ip(), request()->userAgent())
|
||||
: ImageUploadActor::form($user, request()->ip(), request()->userAgent());
|
||||
|
||||
try {
|
||||
$image = $uploader->store(
|
||||
$this->newImage,
|
||||
$pressRelease,
|
||||
$submission,
|
||||
$actor,
|
||||
expectedSha256: $this->provenanceSha,
|
||||
agbUrl: $this->agbUrl(),
|
||||
);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$this->addError($exception->field === 'rights_confirmed' ? 'newRightsConfirmed' : 'newImage', $exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isAdmin) {
|
||||
$customerConfirmation->request($image, request()->ip());
|
||||
}
|
||||
|
||||
$this->resetUploadForm();
|
||||
$this->isUploadFormOpen = false;
|
||||
|
||||
$this->dispatch('title-image-changed');
|
||||
|
||||
Flux::toast(
|
||||
text: $isAdmin
|
||||
? __('Titelbild hochgeladen. Der Kunde wurde per E-Mail um die Bestätigung der Bildrechte gebeten.')
|
||||
: __('Titelbild hochgeladen.'),
|
||||
variant: 'success',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Bestätigungslink erneut an den Kontoinhaber schicken.
|
||||
*/
|
||||
public function resendConfirmationRequest(CustomerImageConfirmation $customerConfirmation): void
|
||||
{
|
||||
$pressRelease = $this->getPressRelease();
|
||||
$this->authorize('update', $pressRelease);
|
||||
|
||||
$image = $this->titleImageFor($pressRelease);
|
||||
|
||||
if ($image === null || !$image->awaitsCustomerConfirmation() || !$this->actsAsAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sent = $customerConfirmation->request($image, request()->ip());
|
||||
|
||||
Flux::toast(
|
||||
text: $sent ? __('Bestätigungslink erneut gesendet.') : __('Kein Empfänger für den Bestätigungslink hinterlegt.'),
|
||||
variant: $sent ? 'success' : 'warning',
|
||||
);
|
||||
}
|
||||
|
||||
public function remove(int $imageId, PressReleaseImageRemover $remover): void
|
||||
{
|
||||
$pressRelease = $this->getPressRelease();
|
||||
$this->authorize('update', $pressRelease);
|
||||
|
||||
if (!$this->canChangeImages($pressRelease)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = $pressRelease->images()->whereKey($imageId)->first();
|
||||
|
||||
if (!$image) {
|
||||
return;
|
||||
}
|
||||
|
||||
$remover->remove($image);
|
||||
|
||||
$this->dispatch('title-image-changed');
|
||||
|
||||
Flux::toast(text: __('Titelbild entfernt.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function with(ImageRightsTextRenderer $texts): array
|
||||
{
|
||||
$pressRelease = $this->getPressRelease();
|
||||
$titleImage = $this->titleImageFor($pressRelease);
|
||||
$legalText = $texts->current();
|
||||
$agbUrl = $this->agbUrl();
|
||||
|
||||
return [
|
||||
'titleImage' => $titleImage,
|
||||
'latestConfirmation' => $titleImage?->latestRightsConfirmation,
|
||||
'awaitsCustomerConfirmation' => $titleImage?->awaitsCustomerConfirmation() ?? false,
|
||||
'canEdit' => auth()->user()?->can('update', $pressRelease) === true && $this->canChangeImages($pressRelease),
|
||||
'isAdminActor' => $this->actsAsAdmin(),
|
||||
'companyName' => $this->companyName(),
|
||||
'legalTextKey' => $legalText?->key,
|
||||
'legalLong' => $legalText ? $texts->renderLong($legalText, $agbUrl) : null,
|
||||
'legalShort' => $legalText ? $texts->renderShort($legalText, $agbUrl) : null,
|
||||
'provenanceState' => $this->provenanceState(),
|
||||
'aiOptions' => AiImageDeclaration::cases(),
|
||||
'focusOptions' => $this->focusOptions(),
|
||||
'maxMb' => (int) round((int) config('images.max_bytes') / 1024 / 1024),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function imageRules(): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'image',
|
||||
'mimes:jpeg,jpg,png,webp',
|
||||
'max:' . (int) ((int) config('images.max_bytes') / 1024),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand des Prüfhinweises im Formular (B.1): B keine Daten, C
|
||||
* vorbelegt, D nicht prüfbar. Zustand A (geprüfte Signatur) kommt mit
|
||||
* Phase 2.
|
||||
*
|
||||
* @return array{key: string, title: string, text: string, tone: string}|null
|
||||
*/
|
||||
private function provenanceState(): ?array
|
||||
{
|
||||
if ($this->provenanceStatus === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match (ImageProvenanceStatus::from($this->provenanceStatus)) {
|
||||
ImageProvenanceStatus::Error => [
|
||||
'key' => 'error',
|
||||
'title' => __('Herkunftsdaten konnten nicht geprüft werden.'),
|
||||
'text' => __('Bitte machen Sie die Angaben selbst. Ihr Upload wird dadurch nicht blockiert.'),
|
||||
'tone' => 'amber',
|
||||
],
|
||||
ImageProvenanceStatus::UnsignedMetadata => [
|
||||
'key' => 'prefilled',
|
||||
'title' => __('Angaben aus den Metadaten der Datei vorbelegt.'),
|
||||
'text' => $this->aiSuggestion !== null
|
||||
? __('Die Datei nennt als Quelle „:type". Sie können die Auswahl ändern.', ['type' => AiImageDeclaration::from($this->aiSuggestion)->label()])
|
||||
: ($this->enhancementHint
|
||||
? __('Die Datei ist als algorithmisch nachbearbeitet gekennzeichnet (z. B. Schärfung). Das allein ist keine KI-Bearbeitung.')
|
||||
: __('Bitte prüfen Sie den vorgeschlagenen Bildnachweis.')),
|
||||
'tone' => 'zinc',
|
||||
],
|
||||
ImageProvenanceStatus::ValidatedTrusted => [
|
||||
'key' => 'verified',
|
||||
'title' => __('Herkunftsdaten erkannt (Content Credentials).'),
|
||||
'text' => __('Die Kennzeichnung ist fest hinterlegt und kann nicht abgeschwächt werden.'),
|
||||
'tone' => 'green',
|
||||
],
|
||||
default => [
|
||||
'key' => 'absent',
|
||||
'title' => __('Keine Herkunftsdaten in der Datei gefunden.'),
|
||||
'text' => __('Bitte machen Sie die Angaben selbst.'),
|
||||
'tone' => 'zinc',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function focusOptions(): array
|
||||
{
|
||||
return [
|
||||
'0,0' => __('oben links'),
|
||||
'50,0' => __('oben'),
|
||||
'100,0' => __('oben rechts'),
|
||||
'0,50' => __('links'),
|
||||
'50,50' => __('Mitte'),
|
||||
'100,50' => __('rechts'),
|
||||
'0,100' => __('unten links'),
|
||||
'50,100' => __('unten'),
|
||||
'100,100' => __('unten rechts'),
|
||||
];
|
||||
}
|
||||
|
||||
private function agbUrl(): string
|
||||
{
|
||||
return route('agb', ['edition' => 'de']);
|
||||
}
|
||||
|
||||
private function companyName(): string
|
||||
{
|
||||
return (string) ($this->getPressRelease()->company()->withoutGlobalScope(PortalScope::class)->value('name') ?? '');
|
||||
}
|
||||
|
||||
private function actsAsAdmin(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
return $user !== null && $user->canAccessAdmin() && $this->getPressRelease()->user_id !== $user->getKey();
|
||||
}
|
||||
|
||||
private function getPressRelease(): PressRelease
|
||||
{
|
||||
return PressRelease::withoutGlobalScope(PortalScope::class)->findOrFail($this->pressReleaseId);
|
||||
}
|
||||
|
||||
private function canChangeImages(PressRelease $pressRelease): bool
|
||||
{
|
||||
if (auth()->user()?->canAccessAdmin()) {
|
||||
return !in_array($pressRelease->status, [PressReleaseStatus::Archived], true);
|
||||
}
|
||||
|
||||
return in_array($pressRelease->status, [PressReleaseStatus::Draft, PressReleaseStatus::Rejected], true);
|
||||
}
|
||||
|
||||
private function titleImageFor(PressRelease $pressRelease): ?PressReleaseImage
|
||||
{
|
||||
return $pressRelease->images()->with('latestRightsConfirmation')->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first();
|
||||
}
|
||||
|
||||
private function resetProvenance(): void
|
||||
{
|
||||
$this->provenanceStatus = null;
|
||||
$this->provenanceSha = null;
|
||||
$this->creditSuggestion = null;
|
||||
$this->aiSuggestion = null;
|
||||
$this->enhancementHint = false;
|
||||
}
|
||||
|
||||
private function resetUploadForm(): void
|
||||
{
|
||||
$this->reset(['newImage', 'newCopyright', 'newAiDeclaration', 'newTitle', 'newDescription', 'newLicenseUrl', 'newSourceUrl', 'newRightsConfirmed', 'noCrop', 'focus', 'showLegalDetails', 'autoCopyright']);
|
||||
$this->resetProvenance();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="md">{{ __('Titelbild') }}</flux:heading>
|
||||
<flux:badge color="{{ $titleImage ? ($awaitsCustomerConfirmation ? 'amber' : 'green') : 'zinc' }}" size="sm">
|
||||
{{ $titleImage ? ($awaitsCustomerConfirmation ? __('wartet auf Kundenbestätigung') : __('gesetzt')) : __('Platzhalter aktiv') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
@if ($titleImage)
|
||||
<div class="mt-4 overflow-hidden rounded-md border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="relative aspect-[16/9] bg-zinc-50 dark:bg-zinc-800">
|
||||
@php
|
||||
$titleImageUrl =
|
||||
$titleImage->variantUrl('cover') ??
|
||||
($titleImage->variantUrl('large') ?? ($titleImage->variantUrl('medium') ?? $titleImage->url()));
|
||||
@endphp
|
||||
|
||||
@if ($titleImageUrl)
|
||||
<img src="{{ $titleImageUrl }}" alt="{{ $titleImage->title ?? __('Titelbild') }}"
|
||||
class="absolute inset-0 size-full object-cover" loading="lazy" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $titleImage->title ?: __('Eigenes Titelbild') }}
|
||||
</div>
|
||||
@if ($titleImage->copyright)
|
||||
<p class="m-0 truncate text-xs text-zinc-500" title="{{ $titleImage->copyright }}">
|
||||
{{ __('Bildnachweis: :copyright', ['copyright' => $titleImage->copyright]) }}
|
||||
</p>
|
||||
@elseif ($titleImage->author)
|
||||
<p class="m-0 truncate text-xs text-zinc-500">© {{ $titleImage->author }}</p>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-1 text-xs text-zinc-400">
|
||||
@if ($aiBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null))
|
||||
<flux:badge color="purple" size="xs">{{ $aiBadge }}</flux:badge>
|
||||
@endif
|
||||
@if ($titleImage->ai_declaration_source)
|
||||
<span>{{ __('Kennzeichnung: :source', ['source' => $titleImage->ai_declaration_source->publicLabel()]) }}</span>
|
||||
@endif
|
||||
@if ($titleImage->width && $titleImage->height)
|
||||
<span>{{ $titleImage->width }}×{{ $titleImage->height }}</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Bestätigungsstand (D.3): Kennung, AGB-Fassung, Zeitpunkt, Kanal. --}}
|
||||
@if ($latestConfirmation)
|
||||
<p class="m-0 text-xs text-zinc-500">
|
||||
{{ __('Rechte bestätigt am :date (:key, AGB :agb, :channel)', [
|
||||
'date' => $latestConfirmation->confirmed_at->format('d.m.Y H:i'),
|
||||
'key' => $latestConfirmation->legal_text_key,
|
||||
'agb' => $latestConfirmation->agb_version ?? '–',
|
||||
'channel' => $latestConfirmation->channel->value,
|
||||
]) }}
|
||||
</p>
|
||||
@elseif ($titleImage->isLegacyRules())
|
||||
<p class="m-0 text-xs text-zinc-400">{{ __('Bestand ohne Bestätigungsprotokoll.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($canEdit)
|
||||
<flux:button size="sm" variant="filled" icon="trash"
|
||||
wire:click="remove({{ $titleImage->id }})"
|
||||
wire:confirm="{{ __('Titelbild wirklich entfernen? Danach wird wieder der Platzhalter verwendet.') }}">
|
||||
{{ __('Titelbild löschen') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($awaitsCustomerConfirmation)
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
{{ __('Dieses Bild wurde im Auftrag hochgeladen. Es wird erst veröffentlicht, wenn der Kontoinhaber die Bildrechte bestätigt hat.') }}
|
||||
@if ($isAdminActor)
|
||||
<flux:button size="xs" variant="ghost" class="ml-2" wire:click="resendConfirmationRequest">
|
||||
{{ __('Bestätigungslink erneut senden') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@elseif($canEdit)
|
||||
<div
|
||||
class="mt-4 flex flex-col gap-4 rounded-md border border-dashed border-zinc-300 p-5 dark:border-zinc-700 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<flux:heading size="xs">{{ __('Hier fehlt ein Titelbild') }}</flux:heading>
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ __('Der Platzhalter bleibt aktiv, bis ein eigenes Titelbild hochgeladen wurde.') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<flux:button type="button" variant="primary" icon="arrow-up-tray" wire:click="openUploadForm">
|
||||
{{ __('Eigenes Titelbild hochladen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<flux:modal wire:model.self="isUploadFormOpen" :dismissible="false" class="w-full max-w-3xl">
|
||||
@if ($isUploadFormOpen)
|
||||
<form wire:submit="saveImage" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Titelbild hochladen') }}</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Bild auswählen, Bildnachweis und KI-Kennzeichnung angeben, bestätigen — fertig.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
{{-- ===== 1 · Bild ===== --}}
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-[11px] font-bold text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">1</span>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bild') }}</span>
|
||||
</div>
|
||||
|
||||
@if (!$newImage)
|
||||
<flux:file-upload wire:model="newImage" accept="image/jpeg,image/png,image/webp"
|
||||
:description="__('JPG/PNG/WebP, max. :mb MB. Wird als Titelbild gespeichert und ersetzt den Platzhalter.', ['mb' => $maxMb])">
|
||||
<flux:file-upload.dropzone :heading="__('Bild hierher ziehen oder klicken')"
|
||||
:text="__('JPG, PNG oder WebP · max. :mb MB', ['mb' => $maxMb])" with-progress />
|
||||
</flux:file-upload>
|
||||
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
{{ __('Bitte laden Sie nur Bilder hoch, für die Sie die erforderlichen Nutzungsrechte besitzen. Bilder aus Google, Social Media, Messenger-Gruppen oder fremden Websites dürfen nicht ohne ausdrückliche Erlaubnis verwendet werden.') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-hidden rounded-md border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="relative aspect-[16/9] bg-zinc-50 dark:bg-zinc-800">
|
||||
@if ($this->newImagePreviewUrl())
|
||||
<img src="{{ $this->newImagePreviewUrl() }}"
|
||||
alt="{{ __('Vorschau des gewählten Titelbilds') }}"
|
||||
class="absolute inset-0 size-full {{ $noCrop ? 'object-contain' : 'object-cover' }}"
|
||||
style="object-position: {{ str_replace(',', '% ', $focus) }}%" />
|
||||
@else
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<flux:icon.photo class="size-10 text-zinc-400" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-zinc-200 px-3 py-2 dark:border-zinc-700">
|
||||
<div class="min-w-0 text-xs text-zinc-500">
|
||||
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $newImage->getClientOriginalName() }}</span>
|
||||
<span class="mx-1">·</span>
|
||||
<span>{{ number_format($newImage->getSize() / 1048576, 2, ',', '.') }} MB</span>
|
||||
</div>
|
||||
<flux:button size="xs" variant="filled" icon="arrow-path" wire:click="removeNewImage">
|
||||
{{ __('Anderes Bild wählen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($provenanceState)
|
||||
<div class="rounded-md border p-3 text-xs leading-relaxed
|
||||
{{ $provenanceState['tone'] === 'amber' ? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100' : ($provenanceState['tone'] === 'green' ? 'border-green-200 bg-green-50 text-green-900 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-100' : 'border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300') }}"
|
||||
data-provenance="{{ $provenanceState['key'] }}">
|
||||
<span class="font-semibold">{{ $provenanceState['title'] }}</span>
|
||||
{{ $provenanceState['text'] }}
|
||||
@if ($provenanceState['key'] !== 'absent')
|
||||
<span class="block mt-1 text-zinc-500">
|
||||
{{ __('Stimmen die Herkunftsdaten nicht? Schreiben Sie uns an :email.', ['email' => config('legal.contact_email')]) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<flux:select wire:model.live="focus" :label="__('Bildausschnitt: wichtiger Bereich')"
|
||||
:description="__('Der Zuschnitt für Vorschauen hält diesen Bereich im Bild.')">
|
||||
@foreach ($focusOptions as $value => $label)
|
||||
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:switch wire:model.live="noCrop" align="right" :label="__('Nicht beschneiden')"
|
||||
:description="__('Das ganze Bild bleibt sichtbar, mit neutralem Rand.')" />
|
||||
</div>
|
||||
@endif
|
||||
<flux:error name="newImage" />
|
||||
</section>
|
||||
|
||||
@if ($newImage)
|
||||
{{-- ===== 2 · Angaben ===== --}}
|
||||
<section class="space-y-4 border-t border-zinc-200 pt-5 dark:border-zinc-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-[11px] font-bold text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">2</span>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bildnachweis & KI-Kennzeichnung') }}</span>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="newCopyright" :label="__('Bildnachweis')" :badge="__('Pflicht')"
|
||||
:description="$creditSuggestion !== null
|
||||
? __('Vorschlag aus den Metadaten der Datei — bitte prüfen. So erscheint der Nachweis unter dem Bild („Foto: …").')
|
||||
: __('Vorschlag: Ihr Firmenname — bitte prüfen. Bei Stock- oder Agenturbildern den Nachweis so eintragen, wie die Lizenz ihn verlangt.')"
|
||||
:placeholder="$companyName ?: __('z. B. Max Mustermann / Beispiel GmbH')" required />
|
||||
|
||||
<flux:radio.group wire:model="newAiDeclaration" :label="__('KI-Kennzeichnung')" :badge="__('Pflicht')"
|
||||
:description="__('KI-generierte oder KI-bearbeitete Bilder erhalten öffentlich einen neutralen Hinweis. Übliche Bearbeitungen wie Zuschnitt, Farbkorrektur oder Schärfung gelten nicht als KI-Bearbeitung.')"
|
||||
required>
|
||||
@foreach ($aiOptions as $aiImageOption)
|
||||
<flux:radio :value="$aiImageOption->value" :label="$aiImageOption->label()" />
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
<flux:error name="newAiDeclaration" />
|
||||
|
||||
<details class="group" @if ($newLicenseUrl || $newSourceUrl || $newTitle || $newDescription) open @endif>
|
||||
<summary class="flex cursor-pointer list-none items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-500 [&::-webkit-details-marker]:hidden">
|
||||
<flux:icon.chevron-right class="size-3.5 transition-transform group-open:rotate-90" />
|
||||
{{ __('Optionale Angaben') }}
|
||||
<span class="font-normal normal-case tracking-normal text-zinc-400">— {{ __('Titel, Bildunterschrift, Lizenz, Quelle') }}</span>
|
||||
</summary>
|
||||
<div class="mt-3 space-y-3">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<flux:input wire:model="newTitle" :label="__('Titel / Alt-Text')"
|
||||
:description="__('Beschreibt das Bild für Suchmaschinen und Screenreader.')" />
|
||||
<flux:input wire:model="newDescription" :label="__('Bildunterschrift')" />
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<flux:input wire:model="newLicenseUrl" type="url" :label="__('Lizenz (Link)')"
|
||||
:description="__('Nur, wenn eine Lizenz die Nutzung regelt, z. B. Creative Commons oder Agentur.')" placeholder="https://…" />
|
||||
<flux:input wire:model="newSourceUrl" type="url" :label="__('Quelle (Link)')"
|
||||
:description="__('Fundstelle oder Ursprungsseite — keine Lizenz.')" placeholder="https://…" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
{{-- ===== 3 · Bestätigung ===== --}}
|
||||
<section class="space-y-3 border-t border-zinc-200 pt-5 dark:border-zinc-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-[11px] font-bold text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">3</span>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bestätigung') }}</span>
|
||||
</div>
|
||||
|
||||
@if ($isAdminActor)
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
{{ __('Sie laden im Auftrag des Kunden hoch. Die Rechtebestätigung gibt der Kontoinhaber selbst ab: Er erhält nach dem Upload einen Link „Bild bestätigen". Bis dahin bleibt der Platzhalter aktiv.') }}
|
||||
</div>
|
||||
@else
|
||||
<flux:switch wire:model="newRightsConfirmed" align="right" :label="__('Ich bestätige')"
|
||||
:description="$legalShort" />
|
||||
<flux:error name="newRightsConfirmed" />
|
||||
|
||||
<button type="button" wire:click="$toggle('showLegalDetails')"
|
||||
class="flex items-center gap-1 text-xs text-zinc-500 underline-offset-2 hover:underline"
|
||||
aria-expanded="{{ $showLegalDetails ? 'true' : 'false' }}">
|
||||
<flux:icon.chevron-right class="size-3.5 transition-transform {{ $showLegalDetails ? 'rotate-90' : '' }}" />
|
||||
{{ __('Vollständiger Wortlaut') }}
|
||||
@if ($legalTextKey)
|
||||
<span class="text-zinc-400">({{ $legalTextKey }})</span>
|
||||
@endif
|
||||
</button>
|
||||
@if ($showLegalDetails && $legalLong)
|
||||
<blockquote class="m-0 rounded-md border border-zinc-200 bg-zinc-50 p-3 text-xs leading-relaxed text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300" data-legal-text>
|
||||
{{ $legalLong }}
|
||||
</blockquote>
|
||||
@endif
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-700">
|
||||
<flux:button type="button" variant="filled" wire:click="closeUploadForm">
|
||||
{{ __('Abbrechen') }}</flux:button>
|
||||
@if ($newImage)
|
||||
<flux:button type="submit" variant="primary" icon="arrow-up-tray">
|
||||
{{ __('Hochladen & einsetzen') }}</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</flux:modal>
|
||||
@else
|
||||
<div class="mt-4 rounded-md border border-dashed border-zinc-300 p-8 text-center dark:border-zinc-700">
|
||||
<flux:icon.photo class="mx-auto size-10 text-zinc-400" />
|
||||
<flux:text class="mt-2 text-sm text-zinc-500">
|
||||
{{ __('Noch kein eigenes Titelbild hinterlegt. Der Platzhalter bleibt aktiv.') }}</flux:text>
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
|
@ -700,7 +700,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
@endif
|
||||
|
||||
{{-- 7) MEDIEN — Image-Manager direkt eingebunden --}}
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
@if (config('images.rights_v2'))
|
||||
<livewire:components.press-release-images-manager-v2 :press-release-id="$id" :wire:key="'pr-images-v2-'.$id" />
|
||||
@else
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
@endif
|
||||
|
||||
{{-- 7) ANHÄNGE — TEMPORÄR DEAKTIVIERT
|
||||
Datei-Uploads erfordern eine vollständige Sicherheitsprüfung
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ use App\Services\PressRelease\AiDeclarationRequiredException;
|
|||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\BookingRequiredException;
|
||||
use App\Services\PressRelease\BoostService;
|
||||
use App\Services\Image\ImageRightsTextRenderer;
|
||||
use App\Services\PressRelease\Images\CustomerImageConfirmation;
|
||||
use App\Services\PressRelease\Images\ImageUploadException;
|
||||
use App\Services\PressRelease\PressReleaseCoverImage;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use App\Services\PressRelease\QuotaExceededException;
|
||||
|
|
@ -105,6 +108,48 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
*/
|
||||
public bool $purchaseConfirmed = false;
|
||||
|
||||
/**
|
||||
* „Bild bestätigen" (Bildkennzeichnung B.4, Decision-Update E10): Schalter
|
||||
* für die Rechtebestätigung eines im Auftrag hochgeladenen Titelbilds.
|
||||
*/
|
||||
public bool $imageRightsConfirmed = false;
|
||||
|
||||
public function confirmImageRights(CustomerImageConfirmation $confirmation, ImageRightsTextRenderer $texts): void
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('update', $pr);
|
||||
|
||||
$image = $pr->images()->with('latestRightsConfirmation')->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first();
|
||||
|
||||
if ($image === null || ! $image->awaitsCustomerConfirmation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate(
|
||||
['imageRightsConfirmed' => ['accepted']],
|
||||
['imageRightsConfirmed.accepted' => __('Die Bestätigung ist erforderlich, damit das Bild veröffentlicht werden kann.')],
|
||||
);
|
||||
|
||||
try {
|
||||
$confirmation->confirm(
|
||||
$image,
|
||||
auth()->user(),
|
||||
$texts->current()?->key,
|
||||
request()->ip(),
|
||||
request()->userAgent(),
|
||||
route('agb', ['edition' => 'de']),
|
||||
);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$this->addError('imageRightsConfirmed', $exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->imageRightsConfirmed = false;
|
||||
|
||||
Flux::toast(text: __('Bildrechte bestätigt. Das Titelbild kann jetzt veröffentlicht werden.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
|
|
@ -822,7 +867,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
'categoryName' => $categoryName,
|
||||
'coverUrl' => $cover->coverUrl($pr, 'cover'),
|
||||
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
|
||||
'titleImage' => $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first(),
|
||||
'titleImage' => $titleImage = $pr->images()->with('latestRightsConfirmation')->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first(),
|
||||
'awaitsImageConfirmation' => $titleImage?->awaitsCustomerConfirmation() === true && auth()->user()->can('update', $pr),
|
||||
'imageLegalShort' => ($imageLegalText = app(ImageRightsTextRenderer::class)->current())
|
||||
? app(ImageRightsTextRenderer::class)->renderShort($imageLegalText, route('agb', ['edition' => 'de']))
|
||||
: null,
|
||||
'imageLegalLong' => $imageLegalText
|
||||
? app(ImageRightsTextRenderer::class)->renderLong($imageLegalText, route('agb', ['edition' => 'de']))
|
||||
: null,
|
||||
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||
'canEdit' => auth()->user()->can('update', $pr)
|
||||
|
|
@ -1710,6 +1762,40 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
@endif
|
||||
</article>
|
||||
|
||||
{{-- ============== BILD BESTÄTIGEN (Admin-Upload, E10) ============== --}}
|
||||
@if ($awaitsImageConfirmation)
|
||||
<article class="panel" data-image-confirmation>
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Titelbild bestätigen') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4 max-w-[760px]">
|
||||
<p class="m-0 text-[13.5px] leading-relaxed">
|
||||
{{ __('Dieses Titelbild wurde von unserem Team für Sie hinterlegt. Es wird erst veröffentlicht, wenn Sie bestätigen, dass Sie zur Veröffentlichung berechtigt sind und die Angaben stimmen.') }}
|
||||
</p>
|
||||
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-[12.5px] m-0">
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Bildnachweis') }}</dt>
|
||||
<dd class="m-0">{{ $titleImage->copyright ?: '—' }}</dd>
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('KI-Kennzeichnung') }}</dt>
|
||||
<dd class="m-0">{{ $titleImage->ai_declaration?->label() ?? '—' }}</dd>
|
||||
</dl>
|
||||
<flux:switch wire:model="imageRightsConfirmed" align="right" :label="__('Ich bestätige')" :description="$imageLegalShort" />
|
||||
<flux:error name="imageRightsConfirmed" />
|
||||
@if ($imageLegalLong)
|
||||
<details class="group">
|
||||
<summary class="flex cursor-pointer list-none items-center gap-1 text-[12px] text-[color:var(--color-ink-3)] [&::-webkit-details-marker]:hidden">
|
||||
<flux:icon.chevron-right class="size-3.5 transition-transform group-open:rotate-90" />
|
||||
{{ __('Vollständiger Wortlaut') }}
|
||||
</summary>
|
||||
<blockquote class="m-0 mt-2 rounded-md border border-[color:var(--color-bg-rule)] p-3 text-[12px] leading-relaxed text-[color:var(--color-ink-3)]">{{ $imageLegalLong }}</blockquote>
|
||||
</details>
|
||||
@endif
|
||||
<div>
|
||||
<flux:button variant="primary" wire:click="confirmImageRights">{{ __('Bildrechte bestätigen') }}</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== INHALT ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue