Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 1m2s
Migration 049: 4 optionale TEXT-Spalten in media_asset_rights_declarations (person_consent_context, parental_consent_context, music_rights_context, third_party_rights_context) fuer Freitext zum Einwilligungskontext. Backend: - media_rights.py: write_rights_declaration speichert 4 Kontextfelder - media_assets.py: copyright_notice + 4 Kontextfelder in Bulk-Upload, RightsDeclarationBody, MediaAssetPatch, MediaBulkPatchBody - exercises.py: copyright_notice + 4 Kontextfelder in upload_exercise_media, wird in INSERT gespeichert Frontend (alle 3 Formulare): - RightsDeclarationDialog: Copyright-Eingabefeld (immer sichtbar) + Freitext-Textarea bei jeder Ja-Antwort (Personen, Minderjaehrige, Musik, Fremdinhalte) - ExerciseInlineFileMediaModal: gleiche Felder inline im Upload-Tab - ExerciseInlineEmbedModal: gleiche Felder inline - api.js: copyright_notice + 4 Kontextfelder in bulkUploadMediaAssets version: 0.8.77 module: media_rights 1.1.0, media_assets 1.14.0, exercises 2.21.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
552 lines
25 KiB
JavaScript
552 lines
25 KiB
JavaScript
/**
|
||
* Modal: Medium aus Archiv verknüpfen oder neue Datei hochladen, dann Inline-Platzhalter §11 einfügen.
|
||
*/
|
||
import React, { useEffect, useState, useCallback, useMemo } from 'react'
|
||
import api from '../utils/api'
|
||
import { resolveMediaAssetFileUrl } from '../utils/exerciseMediaUrl'
|
||
import {
|
||
INLINE_MEDIA_SIZES,
|
||
DEFAULT_INLINE_MEDIA_SIZE,
|
||
sanitizeInlineMediaSize,
|
||
} from '../constants/inlineExerciseMedia'
|
||
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
|
||
|
||
function RtePickerAssetThumb({ asset }) {
|
||
const id = asset.id
|
||
const src = resolveMediaAssetFileUrl(id)
|
||
const mt = (asset.mime_type || '').toLowerCase()
|
||
if (mt.startsWith('image/') && src) {
|
||
return <img alt="" src={src} className="rte-inline-asset-tile__thumb-img" />
|
||
}
|
||
if (mt.startsWith('video/') && src) {
|
||
return (
|
||
<video
|
||
key={`v-${id}`}
|
||
className="rte-inline-asset-tile__thumb-video"
|
||
src={src}
|
||
muted
|
||
playsInline
|
||
preload="metadata"
|
||
onLoadedMetadata={(e) => {
|
||
try {
|
||
const el = e.currentTarget
|
||
const d = el.duration
|
||
el.currentTime = Number.isFinite(d) && d > 0 ? Math.min(0.05, d * 0.01) : 0.05
|
||
} catch (_) {
|
||
/* ignore */
|
||
}
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
const nameLow = String(asset.original_filename || '').toLowerCase()
|
||
if (mt.includes('pdf') || nameLow.endsWith('.pdf')) {
|
||
return <span className="rte-inline-asset-tile__thumb-fallback">PDF</span>
|
||
}
|
||
return <span className="rte-inline-asset-tile__thumb-fallback">Datei</span>
|
||
}
|
||
|
||
/** MIME/Dateiname → Übungs-media_type */
|
||
function inferExerciseMediaType(file) {
|
||
if (!file) return 'image'
|
||
const mime = (file.type || '').toLowerCase()
|
||
if (mime.startsWith('image/')) return 'image'
|
||
if (mime.startsWith('video/')) return 'video'
|
||
if (mime === 'application/pdf' || mime.includes('pdf')) return 'document'
|
||
const name = (file.name || '').toLowerCase()
|
||
if (/\.(mp4|webm|mov|mkv|avi|m4v|mpeg|mpg)$/.test(name)) return 'video'
|
||
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(name)) return 'image'
|
||
if (/\.pdf$/.test(name)) return 'document'
|
||
return 'image'
|
||
}
|
||
|
||
const DECL_INIT = {
|
||
copyright_notice: '',
|
||
rights_holder_confirmed: false,
|
||
contains_identifiable_persons: null,
|
||
person_consent_confirmed: false,
|
||
person_consent_context: '',
|
||
contains_minors: null,
|
||
parental_consent_confirmed: false,
|
||
parental_consent_context: '',
|
||
contains_music: null,
|
||
music_rights_confirmed: false,
|
||
music_rights_context: '',
|
||
contains_third_party_content: null,
|
||
third_party_rights_confirmed: false,
|
||
third_party_rights_context: '',
|
||
}
|
||
|
||
function validateDecl(decl) {
|
||
if (!decl.rights_holder_confirmed)
|
||
return 'Bitte bestätigen, dass du die erforderlichen Rechte an diesem Medium besitzt.'
|
||
if (decl.contains_identifiable_persons === null)
|
||
return 'Bitte angeben, ob erkennbare Personen abgebildet sind.'
|
||
if (decl.contains_identifiable_persons && !decl.person_consent_confirmed)
|
||
return 'Bitte bestätigen, dass die Einwilligungen aller erkennbaren Personen vorliegen.'
|
||
if (decl.contains_minors === null)
|
||
return 'Bitte angeben, ob Minderjährige abgebildet sind.'
|
||
if (decl.contains_minors && !decl.parental_consent_confirmed)
|
||
return 'Bitte bestätigen, dass die Einwilligungen der Sorgeberechtigten vorliegen.'
|
||
if (decl.contains_music === null)
|
||
return 'Bitte angeben, ob das Medium Musik enthält.'
|
||
if (decl.contains_music && !decl.music_rights_confirmed)
|
||
return 'Bitte bestätigen, dass die erforderlichen Musikrechte vorliegen.'
|
||
if (decl.contains_third_party_content === null)
|
||
return 'Bitte angeben, ob fremde geschützte Inhalte enthalten sind.'
|
||
if (decl.contains_third_party_content && !decl.third_party_rights_confirmed)
|
||
return 'Bitte bestätigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen.'
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* open: boolean,
|
||
* onClose: () => void,
|
||
* exerciseId: number,
|
||
* linkedExerciseMedia?: object[],
|
||
* onMediaListChanged: () => Promise<void>,
|
||
* onInserted: (exerciseMediaId: number, displaySize: string, caption?: string) => void,
|
||
* }} props
|
||
*/
|
||
export default function ExerciseInlineFileMediaModal({
|
||
open,
|
||
onClose,
|
||
exerciseId,
|
||
linkedExerciseMedia = [],
|
||
onMediaListChanged,
|
||
onInserted,
|
||
}) {
|
||
const [tab, setTab] = useState('library')
|
||
const [q, setQ] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
const [items, setItems] = useState([])
|
||
const [err, setErr] = useState(null)
|
||
const [selectedAssetId, setSelectedAssetId] = useState(null)
|
||
const [busy, setBusy] = useState(false)
|
||
const [uploadFile, setUploadFile] = useState(null)
|
||
const [uploadTitle, setUploadTitle] = useState('')
|
||
const [displaySize, setDisplaySize] = useState(DEFAULT_INLINE_MEDIA_SIZE)
|
||
const [uploadInputKey, setUploadInputKey] = useState(0)
|
||
const [decl, setDecl] = useState({ ...DECL_INIT })
|
||
const [declErr, setDeclErr] = useState('')
|
||
|
||
const setDeclField = (key, val) => setDecl((d) => ({ ...d, [key]: val }))
|
||
|
||
const assetToExerciseMedia = useMemo(() => {
|
||
const m = new Map()
|
||
for (const row of linkedExerciseMedia || []) {
|
||
const aid = row?.media_asset_id
|
||
if (aid != null) m.set(Number(aid), row)
|
||
}
|
||
return m
|
||
}, [linkedExerciseMedia])
|
||
|
||
const loadAssets = useCallback(async () => {
|
||
setLoading(true)
|
||
setErr(null)
|
||
try {
|
||
const res = await api.listMediaAssets({
|
||
q: q.trim() || undefined,
|
||
limit: 48,
|
||
lifecycle: 'active',
|
||
})
|
||
setItems(Array.isArray(res.items) ? res.items : [])
|
||
} catch (e) {
|
||
setErr(e.message || String(e))
|
||
setItems([])
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [q])
|
||
|
||
useEffect(() => {
|
||
if (!open) return undefined
|
||
setTab('library')
|
||
setSelectedAssetId(null)
|
||
setUploadFile(null)
|
||
setUploadTitle('')
|
||
setUploadInputKey((k) => k + 1)
|
||
setDisplaySize(DEFAULT_INLINE_MEDIA_SIZE)
|
||
setErr(null)
|
||
setDecl({ ...DECL_INIT })
|
||
setDeclErr('')
|
||
const t = setTimeout(loadAssets, 280)
|
||
return () => clearTimeout(t)
|
||
}, [open])
|
||
|
||
useEffect(() => {
|
||
if (!open || tab !== 'library') return undefined
|
||
const t = setTimeout(loadAssets, 300)
|
||
return () => clearTimeout(t)
|
||
}, [q, open, tab, loadAssets])
|
||
|
||
const handleLinkSelected = async () => {
|
||
if (!selectedAssetId) {
|
||
alert('Bitte ein Archiv-Medium auswählen.')
|
||
return
|
||
}
|
||
const size = sanitizeInlineMediaSize(displaySize)
|
||
const assetMeta = items.find((x) => x.id === selectedAssetId)
|
||
const capFromExisting = (row) =>
|
||
sanitizeInlineMediaCaption(row?.original_filename || row?.title || assetMeta?.original_filename || '')
|
||
|
||
const existing = assetToExerciseMedia.get(Number(selectedAssetId))
|
||
if (existing?.id != null) {
|
||
onInserted(Number(existing.id), size, capFromExisting(existing))
|
||
onClose()
|
||
return
|
||
}
|
||
|
||
setBusy(true)
|
||
setErr(null)
|
||
try {
|
||
const row = await api.attachExerciseMediaFromAsset(exerciseId, {
|
||
media_asset_id: selectedAssetId,
|
||
title: '',
|
||
description: '',
|
||
context: 'ablauf',
|
||
is_primary: false,
|
||
})
|
||
const mid = row?.id
|
||
if (mid == null) {
|
||
throw new Error('Antwort ohne exercise_media-ID')
|
||
}
|
||
await onMediaListChanged()
|
||
onInserted(
|
||
Number(mid),
|
||
size,
|
||
sanitizeInlineMediaCaption(assetMeta?.original_filename || ''),
|
||
)
|
||
onClose()
|
||
} catch (e) {
|
||
const msg = e.message || String(e)
|
||
setErr(msg)
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
const handleUploadAndInsert = async () => {
|
||
if (!uploadFile) {
|
||
alert('Bitte eine Datei wählen.')
|
||
return
|
||
}
|
||
const validErr = validateDecl(decl)
|
||
if (validErr) {
|
||
setDeclErr(validErr)
|
||
return
|
||
}
|
||
setDeclErr('')
|
||
const size = sanitizeInlineMediaSize(displaySize)
|
||
const inferred = inferExerciseMediaType(uploadFile)
|
||
const fd = new FormData()
|
||
fd.append('file', uploadFile)
|
||
fd.append('media_type', inferred)
|
||
fd.append('title', uploadTitle.trim())
|
||
fd.append('description', '')
|
||
fd.append('context', 'ablauf')
|
||
fd.append('is_primary', 'false')
|
||
for (const [k, v] of Object.entries(decl)) {
|
||
fd.append(k, String(v))
|
||
}
|
||
setBusy(true)
|
||
setErr(null)
|
||
try {
|
||
const row = await api.uploadExerciseMedia(exerciseId, fd)
|
||
const mid = row?.id
|
||
if (mid == null) {
|
||
throw new Error('Antwort ohne exercise_media-ID')
|
||
}
|
||
await onMediaListChanged()
|
||
const cap = sanitizeInlineMediaCaption(
|
||
uploadTitle.trim() || uploadFile.name || '',
|
||
)
|
||
onInserted(Number(mid), size, cap)
|
||
setUploadFile(null)
|
||
setUploadTitle('')
|
||
setUploadInputKey((k) => k + 1)
|
||
onClose()
|
||
} catch (e) {
|
||
if (e.code === 'MEDIA_ASSET_IN_TRASH' && e.payload?.media_asset_id != null) {
|
||
alert(
|
||
'Dieselbe Datei existiert bereits im Papierkorb — bitte in der Medienbibliothek reaktivieren oder eine andere Datei wählen.',
|
||
)
|
||
} else {
|
||
alert(e.message || String(e))
|
||
}
|
||
setErr(e.message || String(e))
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
const selectedLinked = selectedAssetId != null && assetToExerciseMedia.has(Number(selectedAssetId))
|
||
|
||
if (!open) return null
|
||
|
||
return (
|
||
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && onClose()}>
|
||
<div
|
||
className="admin-modal-sheet rte-inline-media-modal"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="rte-inline-file-title"
|
||
style={{
|
||
maxWidth: '560px',
|
||
width: '100%',
|
||
maxHeight: '90vh',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="admin-modal-sheet__header">
|
||
<h3 id="rte-inline-file-title" className="admin-modal-sheet__title">
|
||
Medium im Textfeld
|
||
</h3>
|
||
<button type="button" className="btn btn-secondary admin-modal-sheet__close" disabled={busy} onClick={onClose}>
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
className={`btn btn-secondary ${tab === 'library' ? 'rte-tab--active' : ''}`}
|
||
style={{ fontSize: '13px' }}
|
||
onClick={() => setTab('library')}
|
||
disabled={busy}
|
||
>
|
||
Aus Mediathek
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`btn btn-secondary ${tab === 'upload' ? 'rte-tab--active' : ''}`}
|
||
style={{ fontSize: '13px' }}
|
||
onClick={() => setTab('upload')}
|
||
disabled={busy}
|
||
>
|
||
Neu hochladen
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ overflowY: 'auto', flex: 1, padding: '12px 14px' }}>
|
||
{err && !loading ? (
|
||
<p style={{ color: 'var(--danger)', marginTop: '0', marginBottom: '12px', fontSize: '0.9rem' }}>{err}</p>
|
||
) : null}
|
||
{tab === 'library' && (
|
||
<>
|
||
<label className="form-label">Suche in der Bibliothek</label>
|
||
<input
|
||
className="form-input"
|
||
value={q}
|
||
onChange={(e) => setQ(e.target.value)}
|
||
placeholder="Name, Tag, © …"
|
||
disabled={busy}
|
||
/>
|
||
{loading ? <p style={{ color: 'var(--text3)', marginTop: '12px' }}>Laden…</p> : null}
|
||
<div className="rte-inline-asset-grid" style={{ marginTop: '14px' }}>
|
||
{items.map((it) => {
|
||
const id = it.id
|
||
const selected = selectedAssetId === id
|
||
const label = it.original_filename || it.copyright_notice || `Archiv #${id}`
|
||
const linked = assetToExerciseMedia.has(Number(id))
|
||
return (
|
||
<button
|
||
key={id}
|
||
type="button"
|
||
className={`rte-inline-asset-tile${selected ? ' rte-inline-asset-tile--selected' : ''}`}
|
||
onClick={() => setSelectedAssetId(id)}
|
||
disabled={busy}
|
||
>
|
||
<div className="rte-inline-asset-tile__thumb" aria-hidden>
|
||
<RtePickerAssetThumb asset={it} />
|
||
</div>
|
||
{linked ? (
|
||
<span className="rte-inline-asset-tile__badge">Bereits verknüpft</span>
|
||
) : null}
|
||
<span className="rte-inline-asset-tile__meta">
|
||
{(it.mime_type || '').split('/')[0] || 'datei'}
|
||
</span>
|
||
<span className="rte-inline-asset-tile__name">{label}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
{!loading && items.length === 0 ? (
|
||
<p style={{ color: 'var(--text3)', marginTop: '12px' }}>Keine Treffer — Suche anpassen oder „Neu hochladen".</p>
|
||
) : null}
|
||
</>
|
||
)}
|
||
|
||
{tab === 'upload' && (
|
||
<>
|
||
<label className="form-label">Datei</label>
|
||
<div className="rte-inline-file-row">
|
||
<input
|
||
key={uploadInputKey}
|
||
id="rte-inline-file-upload-input"
|
||
type="file"
|
||
accept="image/*,video/*,application/pdf"
|
||
className="rte-inline-file-input-hidden"
|
||
disabled={busy}
|
||
onChange={(e) => {
|
||
const f = e.target.files?.[0] || null
|
||
setUploadFile(f)
|
||
}}
|
||
/>
|
||
<label htmlFor="rte-inline-file-upload-input" className="btn btn-secondary rte-inline-file-pick-btn">
|
||
Datei auswählen
|
||
</label>
|
||
<span className="rte-inline-file-name" title={uploadFile?.name || ''}>
|
||
{uploadFile ? uploadFile.name : 'Keine Datei ausgewählt'}
|
||
</span>
|
||
</div>
|
||
<label className="form-label" style={{ marginTop: '12px' }}>
|
||
Titel (optional)
|
||
</label>
|
||
<input
|
||
className="form-input"
|
||
value={uploadTitle}
|
||
onChange={(e) => setUploadTitle(e.target.value)}
|
||
disabled={busy}
|
||
/>
|
||
|
||
{/* P-06 Rechte-Erklärung */}
|
||
<div style={{ marginTop: '18px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}>
|
||
<p style={{ fontSize: '0.82rem', fontWeight: 600, marginBottom: '10px', color: 'var(--text2)' }}>
|
||
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG – p06-v1-conservative)</span>
|
||
</p>
|
||
|
||
<div style={{ marginBottom: '10px' }}>
|
||
<label htmlFor="up-cr" style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: '3px' }}>Copyright-Angabe (optional)</label>
|
||
<input id="up-cr" type="text" className="form-input"
|
||
placeholder="z.B. © 2026 Lars Stommer, CC BY-NC"
|
||
value={decl.copyright_notice}
|
||
onChange={(e) => setDeclField('copyright_notice', e.target.value)}
|
||
disabled={busy} style={{ fontSize: '0.85rem' }} />
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
|
||
<input type="checkbox" id="up-rhc" checked={decl.rights_holder_confirmed}
|
||
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
|
||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="up-rhc" style={{ fontSize: '0.85rem' }}>
|
||
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
|
||
</label>
|
||
</div>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === true} onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === false} onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_identifiable_persons === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="up-pcc" checked={decl.person_consent_confirmed} onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="up-pcc" style={{ fontSize: '0.85rem' }}>Einwilligungen aller erkennbaren Personen liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Schriftliche Einwilligung vom 2026-05-01" value={decl.person_consent_context} onChange={(e) => setDeclField('person_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cm" checked={decl.contains_minors === true} onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cm" checked={decl.contains_minors === false} onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_minors === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="up-pcc2" checked={decl.parental_consent_confirmed} onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="up-pcc2" style={{ fontSize: '0.85rem' }}>Einwilligungen der Sorgeberechtigten liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Elterliche Einwilligung per E-Mail vom 2026-04-15" value={decl.parental_consent_context} onChange={(e) => setDeclField('parental_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cmu" checked={decl.contains_music === true} onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cmu" checked={decl.contains_music === false} onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_music === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="up-mrc" checked={decl.music_rights_confirmed} onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="up-mrc" style={{ fontSize: '0.85rem' }}>Musikrechte (GEMA / Lizenz) liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Lizenz / GEMA-Kontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. CC BY 4.0 oder GEMA-Freimeldung Nr. …" value={decl.music_rights_context} onChange={(e) => setDeclField('music_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === true} onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === false} onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_third_party_content === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="up-tprc" checked={decl.third_party_rights_confirmed} onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="up-tprc" style={{ fontSize: '0.85rem' }}>Rechte an allen enthaltenen Fremdmaterialien liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Rechtsgrundlage (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Verbandslogo mit Genehmigung vom 2026-03-10" value={decl.third_party_rights_context} onChange={(e) => setDeclField('third_party_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
{declErr && (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '8px' }}>{declErr}</p>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ padding: '12px 14px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
|
||
<div className="form-row" style={{ marginBottom: '12px' }}>
|
||
<label className="form-label" style={{ marginBottom: '4px' }}>
|
||
Darstellung im Text
|
||
</label>
|
||
<select className="form-input" value={displaySize} onChange={(e) => setDisplaySize(e.target.value)} disabled={busy}>
|
||
{INLINE_MEDIA_SIZES.map((o) => (
|
||
<option key={o.value} value={o.value}>
|
||
{o.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
{tab === 'library' ? (
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-full"
|
||
disabled={busy || !selectedAssetId}
|
||
onClick={handleLinkSelected}
|
||
>
|
||
{busy ? 'Bitte warten…' : selectedLinked ? 'In Text einfügen (bereits verknüpft)' : 'Verknüpfen & in Text einfügen'}
|
||
</button>
|
||
) : (
|
||
<button type="button" className="btn btn-primary btn-full" disabled={busy || !uploadFile} onClick={handleUploadAndInsert}>
|
||
{busy ? 'Hochladen…' : 'Hochladen & in Text einfügen'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|