feat: add media preview functionality to media library
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 28s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 39s
Test Suite / pytest-backend (pull_request) Failing after 1s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 24s

- Introduced a preview feature for media assets, allowing users to view images and videos in a modal overlay.
- Updated the MediaLibraryPage component to handle media selection and display previews based on the media type.
- Enhanced CSS styles for media cards and preview modals to improve user experience and accessibility.
- Updated instructional text to guide users on how to access media previews.
This commit is contained in:
Lars 2026-05-07 14:39:31 +02:00
parent b8453f3f07
commit 3d321857ec
2 changed files with 236 additions and 8 deletions

View File

@ -5628,6 +5628,22 @@ a.analysis-split__nav-item {
cursor: pointer;
color: var(--text1);
}
.media-library__card-thumb-hit {
display: block;
width: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: zoom-in;
text-align: left;
font: inherit;
color: inherit;
-webkit-tap-highlight-color: transparent;
}
.media-library__card-thumb-hit:active {
opacity: 0.96;
}
.media-library__card-thumb-wrap {
aspect-ratio: 1;
background: var(--surface2);
@ -5717,6 +5733,21 @@ a.analysis-split__nav-item {
overflow: hidden;
background: var(--surface2);
}
.media-library__table-thumb-hit {
display: block;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: zoom-in;
font: inherit;
border-radius: 8px;
-webkit-tap-highlight-color: transparent;
}
.media-library__table-thumb-hit:focus-visible {
outline: 2px solid var(--accent, #3b82f6);
outline-offset: 2px;
}
.media-library__table-thumb .media-library__thumb-img,
.media-library__table-thumb .media-library__thumb-video {
width: 56px;
@ -5865,3 +5896,74 @@ a.analysis-split__nav-item {
flex: 1 1 160px;
min-width: 0;
}
/* Vorschau-Modal (Vollbild nah) */
.media-library__overlay--preview {
background: rgba(0, 0, 0, 0.72);
z-index: 210;
}
.media-library__modal--preview {
max-width: min(92vw, 960px);
width: 100%;
max-height: min(94vh, 900px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.media-library__modal--preview .media-library__modal-head {
flex-shrink: 0;
}
.media-library__preview-title {
flex: 1;
min-width: 0;
font-size: 1rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.media-library__modal--preview .media-library__modal-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.media-library__preview-head-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.media-library__preview-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px 20px;
background: var(--surface2);
}
.media-library__preview-img {
max-width: 100%;
max-height: min(78vh, 720px);
width: auto;
height: auto;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}
.media-library__preview-video {
width: 100%;
max-height: min(78vh, 720px);
border-radius: 8px;
background: #000;
}
.media-library__preview-fallback {
text-align: center;
padding: 24px 16px;
max-width: 360px;
}
.media-library__preview-fallback .btn {
margin-top: 12px;
}

View File

@ -80,6 +80,14 @@ function MediaThumb({ mediaId, mimeType }) {
return <div className="media-library__thumb-ph"></div>
}
function previewDisplayKind(mimeType) {
const m = (mimeType || '').toLowerCase()
if (m.startsWith('image/')) return 'image'
if (m.startsWith('video/')) return 'video'
if (m.includes('pdf')) return 'pdf'
return 'other'
}
export default function MediaLibraryPage() {
const { user } = useAuth()
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
@ -102,6 +110,7 @@ export default function MediaLibraryPage() {
const [bulkClubId, setBulkClubId] = useState('')
const [bulkApplyVis, setBulkApplyVis] = useState(false)
const [busy, setBusy] = useState(false)
const [preview, setPreview] = useState(null)
const loadClubs = useCallback(async () => {
try {
@ -143,6 +152,15 @@ export default function MediaLibraryPage() {
return () => clearTimeout(t)
}, [lifecycle, q, loadItems])
useEffect(() => {
if (!preview) return
const onKey = (e) => {
if (e.key === 'Escape') setPreview(null)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [preview])
const toggleSel = (id) => {
setSelected((prev) => {
const n = new Set(prev)
@ -158,6 +176,7 @@ export default function MediaLibraryPage() {
}
const openEdit = (it) => {
setPreview(null)
const p = it.permissions || {}
setModal(it)
setModalDraft({
@ -299,8 +318,8 @@ export default function MediaLibraryPage() {
</div>
</div>
<p className="media-library__intro">
Veröffentlichte Medien (Verein/Plattform) und eigene Uploads. Bearbeiten und Papierkorb über das
Menü pro Medium Bulk unten in der Leiste.
Veröffentlichte Medien (Verein/Plattform) und eigene Uploads. Vorschau: Bild oder Video groß
anklicken. Bearbeiten und Papierkorb über das Menü pro Medium Bulk unten in der Leiste.
</p>
</header>
@ -397,9 +416,16 @@ export default function MediaLibraryPage() {
>
<MoreVertical size={20} />
</button>
<div className="media-library__card-thumb-wrap">
<MediaThumb mediaId={it.id} mimeType={it.mime_type} />
</div>
<button
type="button"
className="media-library__card-thumb-hit"
onClick={() => setPreview(it)}
title="Vorschau"
>
<div className="media-library__card-thumb-wrap">
<MediaThumb mediaId={it.id} mimeType={it.mime_type} />
</div>
</button>
<div className="media-library__card-footer">
<div className="media-library__card-name" title={it.original_filename || `#${it.id}`}>
{it.original_filename || `Medium #${it.id}`}
@ -439,9 +465,16 @@ export default function MediaLibraryPage() {
<input type="checkbox" checked={selected.has(it.id)} onChange={() => toggleSel(it.id)} />
</td>
<td className="media-library__td-thumb">
<div className="media-library__table-thumb">
<MediaThumb mediaId={it.id} mimeType={it.mime_type} />
</div>
<button
type="button"
className="media-library__table-thumb-hit"
onClick={() => setPreview(it)}
title="Vorschau"
>
<div className="media-library__table-thumb">
<MediaThumb mediaId={it.id} mimeType={it.mime_type} />
</div>
</button>
</td>
<td className="media-library__td-name">{it.original_filename || `#${it.id}`}</td>
<td>{it.visibility}</td>
@ -471,6 +504,99 @@ export default function MediaLibraryPage() {
) : null}
</div>
{preview ? (
<div
className="media-library__overlay media-library__overlay--preview"
role="dialog"
aria-modal="true"
aria-labelledby="preview-media-title"
onClick={() => setPreview(null)}
>
<div
className="media-library__modal media-library__modal--preview"
onClick={(e) => e.stopPropagation()}
>
<div className="media-library__modal-head">
<h2 id="preview-media-title" className="media-library__preview-title">
{preview.original_filename || `Medium #${preview.id}`}
</h2>
<div className="media-library__preview-head-actions">
<button
type="button"
className="btn btn-secondary btn-small"
onClick={() => {
openEdit(preview)
}}
>
Bearbeiten
</button>
<button
type="button"
className="media-library__icon-btn"
onClick={() => setPreview(null)}
aria-label="Vorschau schließen"
>
<X size={22} />
</button>
</div>
</div>
<div className="media-library__preview-body">
{(() => {
const url = resolveMediaAssetFileUrl(preview.id)
const kind = previewDisplayKind(preview.mime_type)
if (!url) {
return <p className="media-library__hint">Keine Datei-URL.</p>
}
if (kind === 'image') {
return (
<img
key={preview.id}
className="media-library__preview-img"
src={url}
alt=""
/>
)
}
if (kind === 'video') {
return (
<video
key={preview.id}
className="media-library__preview-video"
src={url}
controls
playsInline
preload="metadata"
>
Wiedergabe nicht unterstützt.
</video>
)
}
if (kind === 'pdf') {
return (
<div className="media-library__preview-fallback">
<p className="media-library__hint">PDF zur Ansicht im neuen Tab öffnen.</p>
<a className="btn btn-secondary" href={url} target="_blank" rel="noopener noreferrer">
PDF öffnen
</a>
</div>
)
}
return (
<div className="media-library__preview-fallback">
<p className="media-library__hint">
Vorschau für diesen Typ nicht verfügbar ({preview.mime_type || 'unbekannt'}).
</p>
<a className="btn btn-secondary" href={url} target="_blank" rel="noopener noreferrer">
Datei öffnen
</a>
</div>
)
})()}
</div>
</div>
</div>
) : null}
{bulkOpen ? (
<div className="media-library__overlay" role="dialog" aria-modal="true" aria-labelledby="bulk-title">
<div className="media-library__modal media-library__modal--wide">