2026-05-08 12:00:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Modal: Embed-URL als exercise_media anlegen und §11-Platzhalter einfügen.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
|
|
|
|
import api from '../utils/api'
|
|
|
|
|
|
import {
|
|
|
|
|
|
INLINE_MEDIA_SIZES,
|
|
|
|
|
|
DEFAULT_INLINE_MEDIA_SIZE,
|
|
|
|
|
|
sanitizeInlineMediaSize,
|
|
|
|
|
|
} from '../constants/inlineExerciseMedia'
|
2026-05-08 12:20:24 +02:00
|
|
|
|
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
|
2026-05-11 08:44:37 +02:00
|
|
|
|
|
|
|
|
|
|
const DECL_INIT = {
|
|
|
|
|
|
rights_holder_confirmed: false,
|
|
|
|
|
|
contains_identifiable_persons: null,
|
|
|
|
|
|
person_consent_confirmed: false,
|
|
|
|
|
|
contains_minors: null,
|
|
|
|
|
|
parental_consent_confirmed: false,
|
|
|
|
|
|
contains_music: null,
|
|
|
|
|
|
music_rights_confirmed: false,
|
|
|
|
|
|
contains_third_party_content: null,
|
|
|
|
|
|
third_party_rights_confirmed: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 ''
|
|
|
|
|
|
}
|
2026-05-08 12:00:02 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param {{
|
|
|
|
|
|
* open: boolean,
|
|
|
|
|
|
* onClose: () => void,
|
|
|
|
|
|
* exerciseId: number,
|
|
|
|
|
|
* onMediaListChanged: () => Promise<void>,
|
2026-05-08 12:20:24 +02:00
|
|
|
|
* onInserted: (exerciseMediaId: number, displaySize: string, caption?: string) => void,
|
2026-05-08 12:00:02 +02:00
|
|
|
|
* }} props
|
|
|
|
|
|
*/
|
|
|
|
|
|
export default function ExerciseInlineEmbedModal({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
exerciseId,
|
|
|
|
|
|
onMediaListChanged,
|
|
|
|
|
|
onInserted,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [url, setUrl] = useState('')
|
|
|
|
|
|
const [title, setTitle] = useState('')
|
|
|
|
|
|
const [displaySize, setDisplaySize] = useState(DEFAULT_INLINE_MEDIA_SIZE)
|
|
|
|
|
|
const [busy, setBusy] = useState(false)
|
2026-05-11 08:44:37 +02:00
|
|
|
|
const [decl, setDecl] = useState({ ...DECL_INIT })
|
|
|
|
|
|
const [declErr, setDeclErr] = useState('')
|
|
|
|
|
|
|
|
|
|
|
|
const setDeclField = (key, val) => setDecl((d) => ({ ...d, [key]: val }))
|
2026-05-08 12:00:02 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!open) return
|
|
|
|
|
|
setUrl('')
|
|
|
|
|
|
setTitle('')
|
|
|
|
|
|
setDisplaySize(DEFAULT_INLINE_MEDIA_SIZE)
|
2026-05-11 08:44:37 +02:00
|
|
|
|
setDecl({ ...DECL_INIT })
|
|
|
|
|
|
setDeclErr('')
|
2026-05-08 12:00:02 +02:00
|
|
|
|
}, [open])
|
|
|
|
|
|
|
2026-05-11 08:44:37 +02:00
|
|
|
|
const submit = async () => {
|
2026-05-08 12:00:02 +02:00
|
|
|
|
const u = url.trim()
|
|
|
|
|
|
if (!u) {
|
|
|
|
|
|
alert('Bitte eine Embed-URL eingeben (https://…).')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-11 08:44:37 +02:00
|
|
|
|
const validErr = validateDecl(decl)
|
|
|
|
|
|
if (validErr) {
|
|
|
|
|
|
setDeclErr(validErr)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setDeclErr('')
|
2026-05-08 12:00:02 +02:00
|
|
|
|
const size = sanitizeInlineMediaSize(displaySize)
|
|
|
|
|
|
const fd = new FormData()
|
|
|
|
|
|
fd.append('embed_url', u)
|
|
|
|
|
|
fd.append('media_type', 'video')
|
|
|
|
|
|
fd.append('title', title.trim())
|
|
|
|
|
|
fd.append('description', '')
|
|
|
|
|
|
fd.append('context', 'ablauf')
|
|
|
|
|
|
fd.append('is_primary', 'false')
|
2026-05-11 08:25:00 +02:00
|
|
|
|
for (const [k, v] of Object.entries(decl)) {
|
|
|
|
|
|
fd.append(k, String(v))
|
|
|
|
|
|
}
|
2026-05-08 12:00:02 +02:00
|
|
|
|
setBusy(true)
|
|
|
|
|
|
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()
|
2026-05-08 12:20:24 +02:00
|
|
|
|
const cap = sanitizeInlineMediaCaption(
|
|
|
|
|
|
title.trim() || u.replace(/^https?:\/\//i, '').slice(0, 96),
|
|
|
|
|
|
)
|
|
|
|
|
|
onInserted(Number(mid), size, cap)
|
2026-05-08 12:00:02 +02:00
|
|
|
|
onClose()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBusy(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) return null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-11 08:44:37 +02:00
|
|
|
|
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && onClose()}>
|
2026-05-08 12:00:02 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className="admin-modal-sheet"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
|
aria-labelledby="rte-inline-embed-title"
|
2026-05-11 08:44:37 +02:00
|
|
|
|
style={{ maxWidth: '480px', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }}
|
2026-05-08 12:00:02 +02:00
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="admin-modal-sheet__header">
|
|
|
|
|
|
<h3 id="rte-inline-embed-title" className="admin-modal-sheet__title">
|
|
|
|
|
|
Embed im Textfeld
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary admin-modal-sheet__close" disabled={busy} onClick={onClose}>
|
|
|
|
|
|
Schließen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-11 08:44:37 +02:00
|
|
|
|
<div style={{ overflowY: 'auto', flex: 1, padding: '14px 16px' }}>
|
2026-05-08 12:00:02 +02:00
|
|
|
|
<label className="form-label">Embed-URL</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
placeholder="https://…"
|
|
|
|
|
|
value={url}
|
|
|
|
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label className="form-label" style={{ marginTop: '12px' }}>
|
|
|
|
|
|
Titel (optional)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={title}
|
|
|
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label className="form-label" style={{ marginTop: '12px' }}>
|
|
|
|
|
|
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>
|
2026-05-11 08:44:37 +02:00
|
|
|
|
|
|
|
|
|
|
{/* 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={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
id="emb-rhc"
|
|
|
|
|
|
checked={decl.rights_holder_confirmed}
|
|
|
|
|
|
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
style={{ marginTop: '3px', flexShrink: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label htmlFor="emb-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' }}>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-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="emb-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={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
|
|
|
|
|
<input type="checkbox" id="emb-pcc" checked={decl.person_consent_confirmed}
|
|
|
|
|
|
onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)}
|
|
|
|
|
|
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
|
|
|
|
|
<label htmlFor="emb-pcc" style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
Einwilligungen aller erkennbaren Personen liegen vor. *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</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' }}>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-cm" checked={decl.contains_minors === true}
|
|
|
|
|
|
onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-cm" checked={decl.contains_minors === false}
|
|
|
|
|
|
onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{decl.contains_minors === true && (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
|
|
|
|
|
<input type="checkbox" id="emb-pcc2" checked={decl.parental_consent_confirmed}
|
|
|
|
|
|
onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)}
|
|
|
|
|
|
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
|
|
|
|
|
<label htmlFor="emb-pcc2" style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
Einwilligungen der Sorgeberechtigten liegen vor. *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</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' }}>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-cmu" checked={decl.contains_music === true}
|
|
|
|
|
|
onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-cmu" checked={decl.contains_music === false}
|
|
|
|
|
|
onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{decl.contains_music === true && (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
|
|
|
|
|
<input type="checkbox" id="emb-mrc" checked={decl.music_rights_confirmed}
|
|
|
|
|
|
onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)}
|
|
|
|
|
|
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
|
|
|
|
|
<label htmlFor="emb-mrc" style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
Musikrechte (GEMA / Lizenz) liegen vor. *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</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' }}>
|
|
|
|
|
|
<label style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
<input type="radio" name="emb-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="emb-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={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
|
|
|
|
|
<input type="checkbox" id="emb-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="emb-tprc" style={{ fontSize: '0.85rem' }}>
|
|
|
|
|
|
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
|
|
|
|
|
{declErr && (
|
|
|
|
|
|
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '8px' }}>{declErr}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary btn-full" disabled={busy} onClick={submit}>
|
2026-05-08 12:00:02 +02:00
|
|
|
|
{busy ? 'Speichern…' : 'Hinzufügen & in Text einfügen'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|