feat: implement lazy loading for MediaLibraryPage and optimize video rendering
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 1m43s

- Refactored MediaLibraryPage to use React's lazy loading and Suspense for improved performance during page load.
- Updated video rendering logic to utilize createElement for better flexibility and maintainability.
- Enhanced loading state with a spinner to improve user experience while media content is being fetched.
This commit is contained in:
Lars 2026-05-08 09:02:19 +02:00
parent bebcf5af73
commit c1d1c2d7e0
2 changed files with 53 additions and 33 deletions

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { lazy, Suspense } from 'react'
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Routes, Routes,
@ -32,10 +32,11 @@ import AdminMaturityModelsPage from './pages/AdminMaturityModelsPage'
import TrainerContextsPage from './pages/TrainerContextsPage' import TrainerContextsPage from './pages/TrainerContextsPage'
import MediaWikiImportPage from './pages/MediaWikiImportPage' import MediaWikiImportPage from './pages/MediaWikiImportPage'
import AdminUsersPage from './pages/AdminUsersPage' import AdminUsersPage from './pages/AdminUsersPage'
import MediaLibraryPage from './pages/MediaLibraryPage'
import ActiveClubSwitcher from './components/ActiveClubSwitcher' import ActiveClubSwitcher from './components/ActiveClubSwitcher'
import './app.css' import './app.css'
const MediaLibraryPage = lazy(() => import('./pages/MediaLibraryPage'))
// Bottom Navigation (Mobile) // Bottom Navigation (Mobile)
function Nav({ isAdmin }) { function Nav({ isAdmin }) {
const items = getMainNavItems(isAdmin) const items = getMainNavItems(isAdmin)
@ -159,7 +160,28 @@ function AppRoutes() {
<Route path="profile" element={<Navigate to="/settings" replace />} /> <Route path="profile" element={<Navigate to="/settings" replace />} />
<Route path="settings" element={<AccountSettingsPage />} /> <Route path="settings" element={<AccountSettingsPage />} />
<Route path="settings/system" element={<SettingsSystemInfoPage />} /> <Route path="settings/system" element={<SettingsSystemInfoPage />} />
<Route path="media" element={<MediaLibraryPage />} /> <Route
path="media"
element={
<Suspense
fallback={
<div
style={{
minHeight: '50vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)',
}}
>
<div className="spinner" />
</div>
}
>
<MediaLibraryPage />
</Suspense>
}
/>
<Route path="exercises"> <Route path="exercises">
<Route index element={<ExercisesListPage />} /> <Route index element={<ExercisesListPage />} />
<Route path="new" element={<ExerciseFormPage />} /> <Route path="new" element={<ExerciseFormPage />} />

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useRef } from 'react' import { useEffect, useState, useCallback, useRef, createElement } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { import {
LayoutGrid, LayoutGrid,
@ -162,24 +162,22 @@ function MediaThumb({ mediaId, mimeType }) {
return <div className="media-library__thumb-ph">HEIC</div> return <div className="media-library__thumb-ph">HEIC</div>
} }
if (mime.startsWith('video/')) { if (mime.startsWith('video/')) {
return ( return createElement('video', {
<video className: 'media-library__thumb-video',
className="media-library__thumb-video" src: url,
src={url} muted: true,
muted playsInline: true,
playsInline preload: 'metadata',
preload="metadata" onLoadedMetadata: (e) => {
onLoadedMetadata={(e) => { const el = e.target
const el = e.target try {
try { const d = el.duration
const d = el.duration el.currentTime = Number.isFinite(d) && d > 0 ? Math.min(0.05, d * 0.01) : 0.05
el.currentTime = Number.isFinite(d) && d > 0 ? Math.min(0.05, d * 0.01) : 0.05 } catch {
} catch { /* ignore */
/* ignore */ }
} },
}} })
/>
)
} }
if (mime.startsWith('image/')) { if (mime.startsWith('image/')) {
return ( return (
@ -907,17 +905,17 @@ export default function MediaLibraryPage() {
) )
} }
if (kind === 'video') { if (kind === 'video') {
return ( return createElement(
<video 'video',
key={preview.id} {
className="media-library__preview-video" key: preview.id,
src={url} className: 'media-library__preview-video',
controls src: url,
playsInline controls: true,
preload="metadata" playsInline: true,
> preload: 'metadata',
Wiedergabe nicht unterstützt. },
</video> 'Wiedergabe nicht unterstützt.',
) )
} }
if (kind === 'pdf') { if (kind === 'pdf') {