Titelbild: Bildnachweis als Pflichtfeld, Lizenzdetail-Reset bei Typwechsel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7c30d4ecc
commit
6e0b2b1814
3 changed files with 371 additions and 150 deletions
|
|
@ -0,0 +1,159 @@
|
||||||
|
|
||||||
|
|
||||||
|
**Version:** Juni 2026 **Datum:** 11.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Definition von Boost und Veröffentlichungsnachweis (Launch), des Magic-Link-Zugangs- und Änderungsprozesses sowie der Phase-2-Funktionen (Vorab-Prüfung/Redigieren, Prüfzähler, höheres Prüfkontingent, kostenpflichtige Änderungspfade).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Kontext
|
||||||
|
|
||||||
|
Dieses Update definiert die credit-nahen Funktionen und den Änderungsprozess für bestehende Pressemitteilungen. Leitlinie: kleiner, ehrlicher Launch-Umfang; alles, was an die volle Credit-/Prüf-Mechanik gekoppelt ist, wandert kontrolliert in Phase 2. Gesetzlich verpflichtende Pfade bleiben unabhängig davon ab Launch erreichbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Launch-Funktionen
|
||||||
|
|
||||||
|
### 2.1 Boost (Platzierung)
|
||||||
|
|
||||||
|
- **Was:** bezahlte Hervorhebung einer PM – Platzierung auf Startseite und Branchen-/Kategorieseite
|
||||||
|
- **Gate:** nur **grüne** PMs sind boostbar; gelb/rot nicht
|
||||||
|
- **Mechanik:** nachträglich kaufbar, sobald die PM live und grün ist
|
||||||
|
- **Dauer:** Featured-Zeitraum in Stufen (z. B. 7 / 14 / 30 Tage), Preis gestaffelt
|
||||||
|
- **Bezahlung:** über die Credit-Wallet (kleine Wallet ist Launch-Bestandteil)
|
||||||
|
|
||||||
|
### 2.2 Veröffentlichungsnachweis / PDF
|
||||||
|
|
||||||
|
- **Was:** generiertes PDF „PM XY wurde am … auf … veröffentlicht" inkl. URL, Datum, Vorschau
|
||||||
|
- **Zweck:** Reporting an Vorgesetzte/Kunden – klassische PR-Mitnahme
|
||||||
|
- **Bezahlung:** kleiner Credit-Betrag, keine KI nötig
|
||||||
|
- **Generierung:** on-demand aus vorhandenen PM-Daten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Magic-Link: Zugang & Änderungsprozess
|
||||||
|
|
||||||
|
### 3.1 Zugangsmodell – eine Verwaltung, zwei Eintrittswege
|
||||||
|
|
||||||
|
**Grundsatz:** Die Verwaltung bestehender Pressemitteilungen ist **ein und dieselbe** Funktion im pressekonto. Es gibt nur zwei Wege hinein:
|
||||||
|
|
||||||
|
1. **Registrierter Account** → normaler Login → verwaltet seine PMs direkt im pressekonto.
|
||||||
|
2. **Pressekontakt ohne Account** → **Magic-Link** → loggt sich zu den PMs ein, die mit seiner hinterlegten E-Mail verknüpft sind, und nimmt dort Änderungen vor.
|
||||||
|
|
||||||
|
Der Magic-Link ist also **kein eigenes System**, sondern die Zugangsbrücke für Pressekontakte bzw. Firmen, deren E-Mail-Adressen im System hinterlegt sind, die aber noch nicht direkt registriert sind. Die dahinterliegende Verwaltungsoberfläche und die Änderungspfade sind identisch zum eingeloggten Account.
|
||||||
|
|
||||||
|
**Account-Konvertierung:** Aus dem Magic-Link-Zugang heraus kann der Pressekontakt jederzeit „Permanenten Account anlegen" wählen (Passwort vergeben). Danach läuft der Zugang über den regulären Login – der Magic-Link wird für ihn überflüssig.
|
||||||
|
|
||||||
|
**Authentifizierung (Sicherheit):**
|
||||||
|
|
||||||
|
- Auf jeder PM dezenter Link „Sie sind als Pressekontakt hinterlegt? Pressemitteilung verwalten →"
|
||||||
|
- E-Mail-Eingabe + Captcha; **identische Antwort unabhängig vom Match** (verhindert User-Enumeration)
|
||||||
|
- Bei Match: Magic-Link-Mail mit 30-Min-Token → authentifizierte Session
|
||||||
|
- Dashboard listet alle PMs mit dieser E-Mail als Pressekontakt (über mehrere Firmen/Jahre)
|
||||||
|
|
||||||
|
### 3.2 Änderungs- & Lösch-Pfade (A–G)
|
||||||
|
|
||||||
|
Kein wahlloses Ändern/Löschen – Friction nach Anliegen. Alle Pfade laufen über dieselbe Verwaltung (Login **oder** Magic-Link).
|
||||||
|
|
||||||
|
|Pfad|Anliegen|Kosten|Phase|Public Hint|
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|**A**|Tippfehler/Grammatik|kostenfrei|Launch*|nein|
|
||||||
|
|**B**|Pressekontakt-Daten ändern|kostenfrei|Launch*|nein|
|
||||||
|
|**C**|Inhaltliche Korrektur (sachlicher Fehler)|kostenpflichtig|**Phase 2**|ja|
|
||||||
|
|**D**|Update/Ergänzung (neue Information)|kostenpflichtig|**Phase 2**|ja|
|
||||||
|
|**E**|DSGVO-Anonymisierung|kostenfrei|**Launch (Pflicht)**|nein|
|
||||||
|
|**F**|Persönlichkeitsrechtsverletzung|kostenfrei|**Launch (Pflicht)**|je nach Outcome|
|
||||||
|
|**G**|Depublizieren|kostenpflichtig + Bedenkzeit|**Phase 2**|Tombstone|
|
||||||
|
|
||||||
|
* A/B sind kostenfrei und credit-unabhängig – technisch Launch-fähig, aber kein Muss (siehe offene Punkte).
|
||||||
|
|
||||||
|
**Pfad-Details (Kurzfassung):**
|
||||||
|
|
||||||
|
- **A Tippfehler:** Inline-Editor mit Diff; KI prüft, ob nur kosmetisch → ja: übernommen ohne Hinweis; nein: Umleitung zu C
|
||||||
|
- **B Kontaktdaten:** Formular, direkt übernommen, Versionierung im Hintergrund
|
||||||
|
- **C Korrektur:** Editor + Pflichtfeld „Was war falsch / was ist korrekt?"; KI erlaubt Fakt-Korrektur (Zahl/Datum/Name), blockiert Umschreibung der Aussage; PM erhält Korrektur-Hinweis
|
||||||
|
- **D Update:** Ergänzung wird unten mit Datum angehängt, Original bleibt unverändert; KI-Check auf Spam/Werbung
|
||||||
|
- **E DSGVO:** Aufklärung (Art. 85 DSGVO, Medienprivileg → keine Volllöschung, aber kostenfreie Entfernung personenbezogener Daten); Checkbox-Auswahl (Name, Durchwahl, persönliche E-Mail, Freitext); KI-Plausibilitätscheck; sofort umgesetzt
|
||||||
|
- **F Persönlichkeitsrecht:** Pflichtfelder (betroffene Stelle, Art, Begründung, Belege) → Review-Queue mit KI-Vorklassifikation; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung
|
||||||
|
- **G Depublizieren:** Aufklärungsseite → Begründungspflicht (KI lenkt „veraltet" → D, „falsch/peinlich" → C) → kostenpflichtige Bestätigung → 24–48 h Bedenkzeit mit Widerrufslink → Tombstone (`noindex`, raus aus Listen/Suche, URL bleibt)
|
||||||
|
|
||||||
|
### 3.3 Compliance-Minimum zum Launch (E & F)
|
||||||
|
|
||||||
|
**E und F können nicht auf Phase 2 warten.** Sobald PMs live sind, besteht ein gesetzliches Recht auf Anonymisierung personenbezogener Daten (E) und auf Meldung von Persönlichkeitsrechtsverletzungen (F). Beide müssen ab Tag 1 erreichbar sein – zur Not als einfaches Formular/manueller Prozess statt vollem Wizard. Beide sind **kostenfrei** (eine legitime Rechtsanfrage darf nie hinter einer Gebühr stehen) und damit unabhängig vom Phase-2-Credit-Build.
|
||||||
|
|
||||||
|
### 3.4 Missbrauchsschutz, Edge Cases & Standard-Antwort
|
||||||
|
|
||||||
|
- **Rate-Limit** auf Magic-Link-Anfragen pro E-Mail/IP; Cooldown nach Depublizierung (Widerruf-Fenster); Audit-Log mit IP/User-Agent je Edit-Aktion
|
||||||
|
- **Keine valide E-Mail** (alte connektar-PMs): Fallback „Verifikation per Domain-Inhaberschaft / Impressums-Match", manuelle Prüfung
|
||||||
|
- **E-Mail geändert / Person verlässt Firma:** manuelle Anfrage mit Bestätigung über `info@`/Impressum
|
||||||
|
- **Massenanträge:** Bulk möglich, Friction (Gebühr, Bedenkzeit) wird **pro PM** angewendet
|
||||||
|
- **Standard-Antwort auf unautorisierte Lösch-Mails:** „Änderungen an Pressemitteilungen sind ausschließlich über das Verwaltungs-Portal der jeweiligen PM möglich." → beendet das wahllose „löscht das mal eben"-Mailaufkommen ab Launch
|
||||||
|
|
||||||
|
### 3.5 Abgrenzung zum öffentlichen „Melden"-Button
|
||||||
|
|
||||||
|
||„Melden" (öffentlich)|Änderungs-Flow (autorisiert)|
|
||||||
|
|---|---|---|
|
||||||
|
|Wer|jeder Dritte|nur Pressekontakt (Login/Magic-Link)|
|
||||||
|
|Auth|keine|Login oder Magic-Link|
|
||||||
|
|Use Case|Urheberrecht, Verleumdung, Spam|eigene PM ändern|
|
||||||
|
|Outcome|Quarantäne, Prüfung|siehe Pfade A–G|
|
||||||
|
|
||||||
|
Treffpunkt: Eine über „Melden" angezeigte Persönlichkeitsrechtsverletzung läuft in dieselbe Queue wie Pfad F.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phase 2 (nachgelagert)
|
||||||
|
|
||||||
|
Diese Funktionen entstehen gemeinsam, weil sie alle die Re-Check-/Credit-Mechanik voraussetzen.
|
||||||
|
|
||||||
|
### 4.1 Vorab-Prüfung / Redigieren
|
||||||
|
|
||||||
|
Erzeugt erst die Situation „prüfen, ohne (noch) zu veröffentlichen" und den Edit→Neu-Prüfen-Loop. Damit werden auch die kostenpflichtigen Pfade C/D technisch real.
|
||||||
|
|
||||||
|
### 4.2 Prüfzähler
|
||||||
|
|
||||||
|
- **Eigener Zähler**, getrennt von der Credit-Wallet → „Prüfungen inklusive" bleibt sauberes Versprechen
|
||||||
|
- **Aggregiert pro Account/Monat** gedeckelt (nicht pro PM) → löst Klon-Abuse ohne Klon-Erkennung
|
||||||
|
- **Prüf-Tageslimit** als Burst-Schutz (~10/Tag)
|
||||||
|
- **Overflow:** Zähler leer → weitere Prüfungen ziehen 1 Credit/Prüfung aus der Wallet
|
||||||
|
|
||||||
|
### 4.3 Höheres Prüfkontingent (Tier-gestaffelt)
|
||||||
|
|
||||||
|
|Tier|Freie Prüfungen/Monat|
|
||||||
|
|---|---|
|
||||||
|
|Einzel|4|
|
||||||
|
|Starter|12|
|
||||||
|
|Business|30|
|
||||||
|
|Pro|60|
|
||||||
|
|Agency|120|
|
||||||
|
|
||||||
|
Schnitt ~3–4 Prüfungen pro inkludierter PM; ehrlicher Normalfall (≈2 Prüfungen) stößt nie an. Dies sind die früheren „Bonus-Credit"-Zahlen, umgewidmet zum Prüf-Kontingent.
|
||||||
|
|
||||||
|
### 4.4 Kostenpflichtige Magic-Link-Pfade (C / D / G)
|
||||||
|
|
||||||
|
Kosten-Anker bei 1 Credit = 1 € (zu bestätigen):
|
||||||
|
|
||||||
|
|Pfad|Aktion|Anker|
|
||||||
|
|---|---|---|
|
||||||
|
|C|Inhaltliche Korrektur|≈ 8 Credits|
|
||||||
|
|D|Update/Ergänzung|≈ 4 Credits|
|
||||||
|
|G|Depublizieren|≈ 19–29 Credits + 24–48 h Bedenkzeit|
|
||||||
|
|
||||||
|
Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Aktion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Anti-Zombie-Check (dieser Stand)
|
||||||
|
|
||||||
|
- ✅ Gesetzliche Anfragen (E/F) immer kostenfrei und ab Launch erreichbar
|
||||||
|
- ✅ Kosten nur bei echtem Mehraufwand (Korrektur, Update, Depublizierung), nicht bei Pflicht-Rechten
|
||||||
|
- ✅ Eine Verwaltung, zwei Eintrittswege – keine künstliche Trennung registrierter/unregistrierter Nutzer
|
||||||
|
- ✅ Prüf-Kontingent großzügig genug, dass der Normalfall nie ansteht
|
||||||
|
- ✅ Depublizierung mit Aufklärung + Bedenkzeit statt Hard-Delete – schützt den Kunden vor sich selbst
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Offene Punkte
|
||||||
|
|
||||||
|
- **A/B im Launch?** Kostenfrei und credit-unabhängig → könnten den Mail-Aufwand sofort senken. Entscheidung: A/B mit in den Launch nehmen oder kompletten Änderungs-Wizard (inkl. A/B) erst in Phase 2, zum Launch nur E/F als Pflicht-Minimum.
|
||||||
|
- **Kosten-Anker C/D/G** final bestätigen, sobald das Credit-/Prüf-System gebaut wird.
|
||||||
|
- **Boost-Preisstaffel** (7/14/30 Tage) in Credits festlegen.
|
||||||
|
- **PDF-Preis** in Credits festlegen.
|
||||||
|
|
@ -59,6 +59,17 @@ new class extends Component {
|
||||||
$this->isUploadFormOpen = true;
|
$this->isUploadFormOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beim Wechsel des Lizenztyps das Detail-Feld leeren — sonst klebt
|
||||||
|
* z. B. der zuvor gewählte CC-Wert (cc_by) im Freitextfeld
|
||||||
|
* „Lizenzdetails / Begründung" von „Sonstiges".
|
||||||
|
*/
|
||||||
|
public function updatedNewLicenseType(): void
|
||||||
|
{
|
||||||
|
$this->newLicenseDetail = '';
|
||||||
|
$this->resetErrorBag('newLicenseDetail');
|
||||||
|
}
|
||||||
|
|
||||||
public function closeUploadForm(): void
|
public function closeUploadForm(): void
|
||||||
{
|
{
|
||||||
$this->resetUploadForm();
|
$this->resetUploadForm();
|
||||||
|
|
@ -92,7 +103,7 @@ new class extends Component {
|
||||||
[
|
[
|
||||||
'newImage' => ['required', 'image', 'mimes:jpeg,jpg,png,webp', 'max:' . (int) (ImageService::MAX_PRESS_RELEASE_IMAGE_BYTES / 1024)],
|
'newImage' => ['required', 'image', 'mimes:jpeg,jpg,png,webp', 'max:' . (int) (ImageService::MAX_PRESS_RELEASE_IMAGE_BYTES / 1024)],
|
||||||
'newTitle' => ['nullable', 'string', 'max:120'],
|
'newTitle' => ['nullable', 'string', 'max:120'],
|
||||||
'newCopyright' => ['nullable', 'string', 'max:255'],
|
'newCopyright' => ['required', 'string', 'max:255'],
|
||||||
'newAuthor' => ['required', 'string', 'max:255'],
|
'newAuthor' => ['required', 'string', 'max:255'],
|
||||||
'newLicenseType' => ['required', Rule::enum(ImageLicenseType::class)],
|
'newLicenseType' => ['required', Rule::enum(ImageLicenseType::class)],
|
||||||
'newLicenseDetail' => [$requiresLicenseDetail ? 'required' : 'nullable', 'string', 'max:120'],
|
'newLicenseDetail' => [$requiresLicenseDetail ? 'required' : 'nullable', 'string', 'max:120'],
|
||||||
|
|
@ -104,6 +115,7 @@ new class extends Component {
|
||||||
'newRightsConfirmed' => ['accepted'],
|
'newRightsConfirmed' => ['accepted'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'),
|
||||||
'newAuthor.required' => __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'),
|
'newAuthor.required' => __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'),
|
||||||
'newLicenseType.required' => __('Bitte einen Lizenztyp wählen.'),
|
'newLicenseType.required' => __('Bitte einen Lizenztyp wählen.'),
|
||||||
'newLicenseDetail.required' => __('Bitte die Lizenz genauer angeben.'),
|
'newLicenseDetail.required' => __('Bitte die Lizenz genauer angeben.'),
|
||||||
|
|
@ -355,169 +367,190 @@ new class extends Component {
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<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">
|
<form wire:submit="saveImage"
|
||||||
<flux:heading size="xs">{{ __('Titelbild hochladen') }}</flux:heading>
|
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>
|
||||||
|
|
||||||
{{-- ===== Schritt 1 · Bild auswählen ===== --}}
|
{{-- ===== Schritt 1 · Bild auswählen ===== --}}
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<div class="flex items-center gap-2">
|
<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
|
||||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bild auswählen') }}</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>
|
||||||
</div>
|
<span
|
||||||
|
class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bild auswählen') }}</span>
|
||||||
@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>
|
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
@else
|
|
||||||
{{-- Große Vorschau im Titelbild-Format, damit das Motiv
|
@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>
|
||||||
|
|
||||||
|
<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. --}}
|
vor dem Hochladen wirklich beurteilbar ist. --}}
|
||||||
<div class="overflow-hidden rounded-md border border-zinc-200 dark:border-zinc-700">
|
<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">
|
<div class="relative aspect-[16/9] bg-zinc-50 dark:bg-zinc-800">
|
||||||
@if ($this->newImagePreviewUrl())
|
@if ($this->newImagePreviewUrl())
|
||||||
<img src="{{ $this->newImagePreviewUrl() }}" alt="{{ __('Vorschau des gewählten Titelbilds') }}"
|
<img src="{{ $this->newImagePreviewUrl() }}"
|
||||||
class="absolute inset-0 size-full object-cover" />
|
alt="{{ __('Vorschau des gewählten Titelbilds') }}"
|
||||||
@else
|
class="absolute inset-0 size-full object-cover" />
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
@else
|
||||||
<flux:icon.photo class="size-10 text-zinc-400" />
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
</div>
|
<flux:icon.photo class="size-10 text-zinc-400" />
|
||||||
@endif
|
</div>
|
||||||
</div>
|
@endif
|
||||||
<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>
|
||||||
<div class="min-w-0 text-xs text-zinc-500">
|
<div
|
||||||
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $newImage->getClientOriginalName() }}</span>
|
class="flex flex-wrap items-center justify-between gap-3 border-t border-zinc-200 px-3 py-2 dark:border-zinc-700">
|
||||||
<span class="mx-1">·</span>
|
<div class="min-w-0 text-xs text-zinc-500">
|
||||||
<span>{{ number_format($newImage->getSize() / 1048576, 2, ',', '.') }} MB</span>
|
<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>
|
||||||
<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>
|
||||||
|
|
||||||
|
{{-- ===== 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>
|
</div>
|
||||||
@endif
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
<flux:error name="newImage" />
|
<flux:input wire:model="newTitle" :label="__('Titel / Alt-Text (optional)')" />
|
||||||
</section>
|
<flux:input wire:model="newCopyright" :label="__('Öffentlicher Bildnachweis')" :badge="__('Pflicht')"
|
||||||
|
:placeholder="__('Wird öffentlich angezeigt, z. B. Foto: Max Mustermann / Beispiel GmbH.')"
|
||||||
{{-- ===== Schritt 2 · Öffentliche Bildinfos ===== --}}
|
required />
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{-- ===== 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>
|
|
||||||
|
|
||||||
@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.') }}
|
|
||||||
</div>
|
</div>
|
||||||
@elseif($licenseDetailRequired)
|
</section>
|
||||||
<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">
|
{{-- ===== Schritt 3 · Herkunft & Lizenz ===== --}}
|
||||||
<flux:input wire:model="newLicenseUrl" type="url"
|
<section class="space-y-3 border-t border-zinc-200 pt-5 dark:border-zinc-700">
|
||||||
:label="$licenseUrlRequired ? __('Quelle oder Lizenznachweis-URL') : __(
|
<div class="flex items-center gap-2">
|
||||||
'Quelle oder Lizenznachweis-URL (optional)')"
|
<span
|
||||||
:required="$licenseUrlRequired" placeholder="https://…" />
|
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>
|
||||||
<flux:input wire:model="newSourceUrl" type="url" :label="__('Weitere Quelle / Fundstelle (optional)')"
|
<span
|
||||||
placeholder="https://…" />
|
class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Herkunft & Lizenz') }}</span>
|
||||||
</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>
|
</div>
|
||||||
@endif
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<flux:input wire:model="newAuthor" :label="__('Urheber / Fotograf / Rechteinhaber')"
|
||||||
@if ($showsRightsWarning)
|
:badge="__('Pflicht')" required />
|
||||||
<div
|
<flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" :badge="__('Pflicht')"
|
||||||
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">
|
required>
|
||||||
{{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }}
|
<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>
|
</div>
|
||||||
@endif
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{-- ===== Schritt 5 · Bestätigung ===== --}}
|
@if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
|
||||||
<section class="space-y-3 border-t border-zinc-200 pt-5 dark:border-zinc-700">
|
<flux:select wire:model.live="newLicenseDetail" :label="__('Creative-Commons-Lizenz')"
|
||||||
<div class="flex items-center gap-2">
|
required>
|
||||||
<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>
|
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>
|
||||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Bestätigung') }}</span>
|
@foreach ($ccLicenseOptions as $value => $label)
|
||||||
</div>
|
<flux:select.option :value="$value">{{ $label }}</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
<flux:textarea wire:model="newRightsNotes"
|
<div
|
||||||
:label="__('Interne Hinweise zu Rechten / Freigaben (optional)')" rows="3" />
|
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
|
||||||
|
|
||||||
<flux:switch wire:model="newRightsConfirmed" align="right" :label="__('Rechte bestätigt')"
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
: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.')" />
|
<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>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-700">
|
{{-- ===== Schritt 4 · Personen & Rechte Dritter ===== --}}
|
||||||
<flux:button type="button" variant="filled" wire:click="closeUploadForm">{{ __('Abbrechen') }}</flux:button>
|
<section class="space-y-3 border-t border-zinc-200 pt-5 dark:border-zinc-700">
|
||||||
<flux:button type="submit" variant="primary" icon="arrow-up-tray">{{ __('Hochladen') }}</flux:button>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<span
|
||||||
</section>
|
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>
|
||||||
</form>
|
<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
|
@endif
|
||||||
@else
|
@else
|
||||||
<div class="mt-4 rounded-md border border-dashed border-zinc-300 p-8 text-center dark:border-zinc-700">
|
<div class="mt-4 rounded-md border border-dashed border-zinc-300 p-8 text-center dark:border-zinc-700">
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ test('valid cc upload stores license detail and license url', function () {
|
||||||
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||||
->set('newAuthor', 'Jane Doe')
|
->set('newAuthor', 'Jane Doe')
|
||||||
|
->set('newCopyright', 'Foto: Jane Doe (CC BY 4.0)')
|
||||||
->set('newLicenseType', ImageLicenseType::CreativeCommons->value)
|
->set('newLicenseType', ImageLicenseType::CreativeCommons->value)
|
||||||
->set('newLicenseDetail', 'cc_by')
|
->set('newLicenseDetail', 'cc_by')
|
||||||
->set('newLicenseUrl', 'https://creativecommons.org/licenses/by/4.0/')
|
->set('newLicenseUrl', 'https://creativecommons.org/licenses/by/4.0/')
|
||||||
|
|
@ -243,6 +244,34 @@ test('valid cc upload stores license detail and license url', function () {
|
||||||
expect($image->license_url)->toBe('https://creativecommons.org/licenses/by/4.0/');
|
expect($image->license_url)->toBe('https://creativecommons.org/licenses/by/4.0/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the public image credit is required', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
||||||
|
$this->actingAs($owner);
|
||||||
|
|
||||||
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
|
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||||
|
->set('newAuthor', 'Jane Doe')
|
||||||
|
->set('newLicenseType', ImageLicenseType::Own->value)
|
||||||
|
->set('newPeopleRightsStatus', 'none')
|
||||||
|
->set('newPropertyRightsStatus', 'none')
|
||||||
|
->set('newRightsConfirmed', true)
|
||||||
|
->call('saveImage')
|
||||||
|
->assertHasErrors(['newCopyright']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switching the license type clears the stale license detail', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
||||||
|
$this->actingAs($owner);
|
||||||
|
|
||||||
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
|
->set('newLicenseType', ImageLicenseType::CreativeCommons->value)
|
||||||
|
->set('newLicenseDetail', 'cc_by')
|
||||||
|
->set('newLicenseType', ImageLicenseType::Other->value)
|
||||||
|
->assertSet('newLicenseDetail', '');
|
||||||
|
});
|
||||||
|
|
||||||
test('existing title image hides upload form and can be removed', function () {
|
test('existing title image hides upload form and can be removed', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue