/** * 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' /** 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' } /** * @param {{ * open: boolean, * onClose: () => void, * exerciseId: number, * linkedExerciseMedia?: object[], * onMediaListChanged: () => Promise, * 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 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) 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 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') 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 (
e.target === e.currentTarget && !busy && onClose()}>
e.stopPropagation()} >

Medium im Textfeld

{err && !loading ? (

{err}

) : null} {tab === 'library' && ( <> setQ(e.target.value)} placeholder="Name, Tag, © …" disabled={busy} /> {loading ?

Laden…

: null}
{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)) const src = resolveMediaAssetFileUrl(id) const isImg = (it.mime_type || '').startsWith('image/') const isVid = (it.mime_type || '').startsWith('video/') return ( ) })}
{!loading && items.length === 0 ? (

Keine Treffer — Suche anpassen oder „Neu hochladen“.

) : null} )} {tab === 'upload' && ( <>
{ const f = e.target.files?.[0] || null setUploadFile(f) }} /> {uploadFile ? uploadFile.name : 'Keine Datei ausgewählt'}
setUploadTitle(e.target.value)} disabled={busy} /> )}
{tab === 'library' ? ( ) : ( )}
) }