Titelbild-Upload: Bildrechte in 5 Schritten + große Bildvorschau

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 15:34:40 +00:00
parent 25ea91d85b
commit a7c30d4ecc
2 changed files with 167 additions and 90 deletions

View file

@ -355,107 +355,168 @@ new class extends Component {
</flux:button>
</div>
@else
<form wire:submit="saveImage" class="mt-4 space-y-3 rounded-md border border-zinc-200 p-4 dark:border-zinc-700">
<form wire:submit="saveImage" class="mt-4 space-y-6 rounded-md border border-zinc-200 p-4 dark:border-zinc-700 sm:p-5">
<flux:heading size="xs">{{ __('Titelbild hochladen') }}</flux:heading>
<flux:file-upload wire:model="newImage" accept="image/jpeg,image/png,image/webp"
:description="__('JPG/PNG/WebP, max. 16 MB. Wird als Titelbild gespeichert und ersetzt den Platzhalter.')">
<flux:file-upload.dropzone :heading="__('Bild hierher ziehen oder klicken')"
:text="__('JPG, PNG oder WebP · max. 16 MB')" 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>
@if ($newImage)
<flux:file-item :heading="$newImage->getClientOriginalName()" :image="$this->newImagePreviewUrl()"
:size="$newImage->getSize()">
<x-slot name="actions">
<flux:file-item.remove wire:click="removeNewImage" :aria-label="__('Bild entfernen')" />
</x-slot>
</flux:file-item>
@endif
<flux:input wire:model="newTitle" :label="__('Titel / Alt-Text (optional)')" />
<flux:input wire:model="newCopyright" :label="__('Öffentlicher Bildnachweis')"
:description="__('Wird öffentlich angezeigt, z. B. Foto: Max Mustermann / Beispiel GmbH.')" />
<flux:separator variant="subtle" :text="__('Bildrechte')" />
<flux:input wire:model="newAuthor" :label="__('Urheber / Fotograf / Rechteinhaber (Pflichtfeld)')"
required />
<flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" required>
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>
@foreach ($licenseTypeOptions as $value => $label)
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
@endforeach
</flux:select>
@if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
<flux:select wire:model.live="newLicenseDetail" :label="__('Creative-Commons-Lizenz')" required>
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>
@foreach ($ccLicenseOptions as $value => $label)
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
@endforeach
</flux:select>
<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">
{{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }}
{{-- ===== Schritt 1 · Bild auswählen ===== --}}
<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 auswählen') }}</span>
</div>
@elseif($licenseDetailRequired)
<flux:input wire:model="newLicenseDetail" :label="__('Lizenzdetails / Begründung')"
:description="__('Bitte kurz erklären, warum die Nutzung erlaubt ist.')" required />
@endif
<flux:input wire:model="newLicenseUrl" type="url"
:label="$licenseUrlRequired ? __('Quelle oder Lizenznachweis-URL') : __(
'Quelle oder Lizenznachweis-URL (optional)')"
:required="$licenseUrlRequired" placeholder="https://…" />
@if (! $newImage)
<flux:file-upload wire:model="newImage" accept="image/jpeg,image/png,image/webp"
:description="__('JPG/PNG/WebP, max. 16 MB. Wird als Titelbild gespeichert und ersetzt den Platzhalter.')">
<flux:file-upload.dropzone :heading="__('Bild hierher ziehen oder klicken')"
:text="__('JPG, PNG oder WebP · max. 16 MB')" with-progress />
</flux:file-upload>
<flux:input wire:model="newSourceUrl" type="url" :label="__('Weitere Quelle / Fundstelle (optional)')"
placeholder="https://…" />
<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
{{-- Große Vorschau im Titelbild-Format, damit das Motiv
vor dem Hochladen wirklich beurteilbar ist. --}}
<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 object-cover" />
@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>
@endif
<flux:error name="newImage" />
</section>
<flux:radio.group wire:model.live="newPeopleRightsStatus"
:label="__('Sind erkennbare Personen abgebildet?')" required>
@foreach ($peopleRightsOptions as $value => $label)
<flux:radio :value="$value" :label="$label" />
@endforeach
</flux:radio.group>
@if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true))
<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">
{{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }}
{{-- ===== Schritt 2 · Öffentliche Bildinfos ===== --}}
<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">2</span>
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bildinformationen (öffentlich sichtbar)') }}</span>
</div>
@endif
<flux:radio.group wire:model.live="newPropertyRightsStatus"
:label="__('Sind Marken, Kunstwerke, geschützte Werke oder private Orte sichtbar?')" required>
@foreach ($propertyRightsOptions as $value => $label)
<flux:radio :value="$value" :label="$label" />
@endforeach
</flux:radio.group>
@if ($showsRightsWarning)
<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">
{{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }}
<div class="grid gap-3 sm:grid-cols-2">
<flux:input wire:model="newTitle" :label="__('Titel / Alt-Text (optional)')" />
<flux:input wire:model="newCopyright" :label="__('Öffentlicher Bildnachweis')"
:description="__('Wird öffentlich angezeigt, z. B. Foto: Max Mustermann / Beispiel GmbH.')" />
</div>
@endif
</section>
<flux:textarea wire:model="newRightsNotes"
:label="__('Interne Hinweise zu Rechten / Freigaben (optional)')" rows="3" />
{{-- ===== Schritt 3 · Herkunft & Lizenz ===== --}}
<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">{{ __('Herkunft & Lizenz') }}</span>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<flux:input wire:model="newAuthor" :label="__('Urheber / Fotograf / Rechteinhaber')" :badge="__('Pflicht')"
required />
<flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" :badge="__('Pflicht')" required>
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>
@foreach ($licenseTypeOptions as $value => $label)
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
@endforeach
</flux:select>
</div>
<flux:switch wire:model="newRightsConfirmed" align="right" :label="__('Rechte bestätigt')"
:description="__('Ich bestätige, dass ich über die erforderlichen Rechte zur Veröffentlichung dieses Bildes verfüge und die Verantwortung für die Richtigkeit meiner Angaben übernehme. Dies umfasst Urheberrechte, Nutzungsrechte, Persönlichkeitsrechte abgebildeter Personen sowie gegebenenfalls Marken-, Eigentums- oder sonstige Rechte Dritter. Mir ist bewusst, dass ich für fehlerhafte oder unvollständige Angaben verantwortlich bin.')" />
@if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
<flux:select wire:model.live="newLicenseDetail" :label="__('Creative-Commons-Lizenz')" required>
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>
@foreach ($ccLicenseOptions as $value => $label)
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
@endforeach
</flux:select>
<div class="flex justify-end gap-2">
<flux:button type="button" variant="filled" wire:click="closeUploadForm">{{ __('Abbrechen') }}</flux:button>
<flux:button type="submit" variant="primary" icon="arrow-up-tray">{{ __('Hochladen') }}</flux:button>
</div>
<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">
{{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }}
</div>
@elseif($licenseDetailRequired)
<flux:input wire:model="newLicenseDetail" :label="__('Lizenzdetails / Begründung')"
:description="__('Bitte kurz erklären, warum die Nutzung erlaubt ist.')" required />
@endif
<div class="grid gap-3 sm:grid-cols-2">
<flux:input wire:model="newLicenseUrl" type="url"
:label="$licenseUrlRequired ? __('Quelle oder Lizenznachweis-URL') : __(
'Quelle oder Lizenznachweis-URL (optional)')"
:required="$licenseUrlRequired" placeholder="https://…" />
<flux:input wire:model="newSourceUrl" type="url" :label="__('Weitere Quelle / Fundstelle (optional)')"
placeholder="https://…" />
</div>
</section>
{{-- ===== Schritt 4 · Personen & Rechte Dritter ===== --}}
<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">4</span>
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Personen & Rechte Dritter') }}</span>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:radio.group wire:model.live="newPeopleRightsStatus"
:label="__('Sind erkennbare Personen abgebildet?')" required>
@foreach ($peopleRightsOptions as $value => $label)
<flux:radio :value="$value" :label="$label" />
@endforeach
</flux:radio.group>
<flux:radio.group wire:model.live="newPropertyRightsStatus"
:label="__('Sind Marken, Kunstwerke, geschützte Werke oder private Orte sichtbar?')" required>
@foreach ($propertyRightsOptions as $value => $label)
<flux:radio :value="$value" :label="$label" />
@endforeach
</flux:radio.group>
</div>
@if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true))
<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">
{{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }}
</div>
@endif
@if ($showsRightsWarning)
<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">
{{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }}
</div>
@endif
</section>
{{-- ===== Schritt 5 · 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">5</span>
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bestätigung') }}</span>
</div>
<flux:textarea wire:model="newRightsNotes"
:label="__('Interne Hinweise zu Rechten / Freigaben (optional)')" rows="3" />
<flux:switch wire:model="newRightsConfirmed" align="right" :label="__('Rechte bestätigt')"
:description="__('Ich bestätige, dass ich über die erforderlichen Rechte zur Veröffentlichung dieses Bildes verfüge und die Verantwortung für die Richtigkeit meiner Angaben übernehme. Dies umfasst Urheberrechte, Nutzungsrechte, Persönlichkeitsrechte abgebildeter Personen sowie gegebenenfalls Marken-, Eigentums- oder sonstige Rechte Dritter. Mir ist bewusst, dass ich für fehlerhafte oder unvollständige Angaben verantwortlich bin.')" />
<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>
<flux:button type="submit" variant="primary" icon="arrow-up-tray">{{ __('Hochladen') }}</flux:button>
</div>
</section>
</form>
@endif
@else