shinkan-jinkendo/frontend/src/components/ExerciseAttachmentMediaStrip.jsx
Lars 24bf3f7035
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 1m1s
feat(P-13): update app version to 0.8.89, implement MediaPreviewModal and reporting functionality across media components
2026-05-11 18:43:05 +02:00

93 lines
3.8 KiB
JavaScript

/**
* Nur Medien, die noch nicht im Fließtext eingebettet sind — ohne Doppel-Darstellung.
*/
import React, { useMemo, useState } from 'react'
import ExerciseMediaEmbed from './ExerciseMediaEmbed'
import ExerciseMediaThumbTile from './ExerciseMediaThumbTile'
import MediaPreviewModal from './MediaPreviewModal'
import ReportContentModal from './ReportContentModal'
import { resolveExerciseMediaFileUrl } from '../utils/exerciseMediaUrl'
import { collectInlineExerciseMediaIdsFromExercise } from '../utils/exerciseInlineMediaRefs'
function isTrashHidden(m) {
return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden'
}
export default function ExerciseAttachmentMediaStrip({ exerciseId, exercise }) {
const [preview, setPreview] = useState(null)
const [reportTarget, setReportTarget] = useState(null)
const inlineIds = useMemo(() => collectInlineExerciseMediaIdsFromExercise(exercise), [exercise])
const orphans = useMemo(() => {
const list = (exercise?.media || []).filter((m) => m && !isTrashHidden(m))
return list.filter((m) => !inlineIds.has(Number(m.id)))
}, [exercise, inlineIds])
if (!orphans.length || exerciseId == null) return null
return (
<section className="card exercise-detail-section exercise-attachment-media-strip">
<h2>Angehängte Medien</h2>
<p style={{ marginTop: '6px', color: 'var(--text2)', fontSize: '0.88rem' }}>
Hier erscheinen nur Verknüpfungen, die noch nicht im Fließtext eingebettet sind (reine Material-Anhänge).
</p>
<div className="exercise-orphan-media-grid">
{orphans.map((m) => {
const lc = String(m.asset_lifecycle_state || 'active').toLowerCase()
const caption = (m.title || '').trim() || (m.original_filename || '').trim() || `Medium #${m.id}`
return (
<article key={m.id} className="exercise-orphan-media-card">
<div className="exercise-orphan-media-card__head">
<ExerciseMediaThumbTile
exerciseId={exerciseId}
media={m}
onOpenPreview={setPreview}
size={88}
/>
<div className="exercise-orphan-media-card__meta">
<strong className="exercise-orphan-media-card__title">{caption}</strong>
<span className="exercise-orphan-media-card__sub">
#{m.id}
{m.embed_platform ? ` · ${m.embed_platform}` : ''}
{m.media_type ? ` · ${m.media_type}` : ''}
</span>
{lc === 'trash_soft' && (
<span className="exercise-orphan-media-card__warn">Papierkorb (Stufe 1)</span>
)}
</div>
</div>
<ExerciseMediaEmbed exerciseId={exerciseId} media={m} layoutSize="medium" />
</article>
)
})}
</div>
{preview && (
<MediaPreviewModal
title={(preview.title || '').trim() || preview.original_filename || `Medium #${preview.id}`}
media={preview}
fileUrl={preview.embed_url ? null : resolveExerciseMediaFileUrl(exerciseId, preview)}
onClose={() => setPreview(null)}
onReport={
(preview.asset_lifecycle_state || 'active') === 'active' && !preview.asset_legal_hold_active
? () => {
setReportTarget(preview)
setPreview(null)
}
: null
}
/>
)}
{reportTarget && (
<ReportContentModal
targetType="media_asset"
targetId={reportTarget.media_asset_id || reportTarget.id}
targetLabel={reportTarget.title || reportTarget.original_filename || `Medium #${reportTarget.id}`}
onClose={() => setReportTarget(null)}
/>
)}
</section>
)
}