diff --git a/.env.example b/.env.example index f1da55d..cce781b 100644 --- a/.env.example +++ b/.env.example @@ -8,16 +8,18 @@ # ─── Typische Werte PROD (docker-compose.yml) ───────────────────────────────── # DB_NAME=shinkan # DB_USER=shinkan_user -# SHINKAN_MEDIA_HOST=/shinkan-media -# MEDIA_ROOT=/app/media -# … +# DB_PASSWORD=… +# APP_URL=https://shinkan.jinkendo.de +# ALLOWED_ORIGINS=https://shinkan.jinkendo.de +# ENVIRONMENT=production # ─── Typische Werte DEV (docker-compose.dev-env.yml) ───────────────────────── # DB_NAME=shinkan_dev # DB_USER=shinkan_dev -# SHINKAN_MEDIA_HOST=/shinkan-media/dev -# MEDIA_ROOT=/app/media -# … +# DB_PASSWORD=dev_password +# APP_URL=https://dev.shinkan.jinkendo.de +# ALLOWED_ORIGINS=https://dev.shinkan.jinkendo.de,http://192.168.2.49:3098 +# ENVIRONMENT=development # ─── Ab hier: eine ausfüllbare Vorlage (bei uns meist Prod-Defaults) ─────────── DB_HOST=postgres @@ -44,10 +46,7 @@ APP_URL=https://shinkan.jinkendo.de ALLOWED_ORIGINS=https://shinkan.jinkendo.de ENVIRONMENT=production -# Host-Pfad (Bind-Mount). Compose-Defaults: Prod /shinkan-media, Dev /shinkan-media/dev — hier nur setzen zum Überschreiben. -SHINKAN_MEDIA_HOST=/shinkan-media -# Optional: Pfad im Container (FastAPI MEDIA_ROOT); Standard reicht fast immer. -MEDIA_ROOT=/app/media +MEDIA_DIR=/app/media MEDIAWIKI_API_URL=https://karatetrainer.net/api.php MEDIAWIKI_USER=Jinkendo diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml index cf158c8..231da53 100644 --- a/docker-compose.dev-env.yml +++ b/docker-compose.dev-env.yml @@ -1,4 +1,5 @@ -# Medien: Host-Pfad SHINKAN_MEDIA_HOST in .env überschreiben; Default /shinkan-media/dev (getrennt von Prod). +# Keine festen container_name — Compose-Namen haben Projektprefix (-postgres-1). +# Gleiche Variablennamen wie docker-compose.yml; andere Werte in einer eigenen .env neben dieser Datei. services: postgres: @@ -42,9 +43,8 @@ services: MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" - MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" volumes: - - ${SHINKAN_MEDIA_HOST:-/shinkan-media/dev}:${MEDIA_ROOT:-/app/media} + - dev-shinkan-media:/app/media ports: - "8098:8000" depends_on: @@ -70,6 +70,7 @@ services: volumes: dev-shinkan-db-data: + dev-shinkan-media: networks: dev-shinkan-network: diff --git a/docker-compose.yml b/docker-compose.yml index f6b2596..b41c62e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,10 +52,8 @@ services: MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" - # Medien: Container-Pfad MEDIA_ROOT; Host-Pfad SHINKAN_MEDIA_HOST (in .env überschreiben; Default /shinkan-media). - MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" volumes: - - ${SHINKAN_MEDIA_HOST:-/shinkan-media}:${MEDIA_ROOT:-/app/media} + - shinkan-media:/app/media ports: - "8003:8000" depends_on: @@ -81,6 +79,7 @@ services: volumes: shinkan-db-data: + shinkan-media: networks: shinkan-network: diff --git a/frontend/src/ErrorBoundary.jsx b/frontend/src/ErrorBoundary.jsx deleted file mode 100644 index 540bd68..0000000 --- a/frontend/src/ErrorBoundary.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react' - -/** - * Fängt Render-Fehler ab — verhindert „weißen Bildschirm“ ohne Hinweis (z. B. nach Deploy/Chaches). - */ -export default class ErrorBoundary extends React.Component { - constructor(props) { - super(props) - this.state = { error: null } - } - - static getDerivedStateFromError(error) { - return { error } - } - - componentDidCatch(error, info) { - console.error('ErrorBoundary:', error, info?.componentStack) - } - - render() { - if (this.state.error) { - const msg = this.state.error?.message || String(this.state.error) - return ( -
-

Ein Fehler ist aufgetreten

-

- Die Oberfläche konnte nicht geladen werden. Details siehe unten oder in der - Browser-Konsole (F12). Nach einem Deploy einen harten Reload (Cache leeren) - versuchen. -

-
-            {msg}
-          
-

- -

-
- ) - } - return this.props.children - } -} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b264038..8d63283 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,13 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' -import ErrorBoundary from './ErrorBoundary.jsx' import './app.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - + , ) diff --git a/frontend/src/pages/ExerciseFormPage.jsx b/frontend/src/pages/ExerciseFormPage.jsx index 24d7696..c85fe02 100644 --- a/frontend/src/pages/ExerciseFormPage.jsx +++ b/frontend/src/pages/ExerciseFormPage.jsx @@ -6,10 +6,6 @@ import RichTextEditor from '../components/RichTextEditor' import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel' import { SKILL_LEVEL_OPTIONS, normalizeSkillLevelSlug } from '../constants/skillLevels' -/** Kein image/*/video/* — WebKit (iOS) ist damit oft kaputt. */ -const EXERCISE_MEDIA_UPLOAD_ACCEPT = - 'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf' - /** Kachelvorschau: Video nutzt ersten Frame (metadata), Bild = img. */ function ExerciseMediaThumbTile({ exerciseId, media, onOpenPreview }) { const src = !media.embed_url ? resolveExerciseMediaFileUrl(exerciseId, media) : null @@ -1464,7 +1460,7 @@ function ExerciseFormPage() { setMediaFile(e.target.files?.[0] || null)} />
diff --git a/frontend/src/pages/MediaLibraryPage.jsx b/frontend/src/pages/MediaLibraryPage.jsx index 4ad8f22..3ff9883 100644 --- a/frontend/src/pages/MediaLibraryPage.jsx +++ b/frontend/src/pages/MediaLibraryPage.jsx @@ -13,8 +13,8 @@ import { CircleDot, FilePenLine, Copyright, - Image as LucideImage, - Video as LucideVideo, + Image, + Video, FileText, File, Upload, @@ -45,10 +45,6 @@ const MEDIA_KIND_OPTIONS = [ { value: 'other', label: 'Sonstiges' }, ] -/** Kein image/*/video/* — WebKit (iOS Safari) ist damit oft kaputt; alles explizit. */ -const MEDIA_UPLOAD_ACCEPT = - 'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf' - const LC_STATUS_LABELS = { active: 'Aktiv', trash_soft: 'Papierkorb (1)', @@ -219,8 +215,8 @@ function MediaTypeGlyph({ mimeType, compact }) { const kind = previewDisplayKind(mimeType) const label = MEDIA_KIND_LABELS[kind] || 'Medium' let Icon = File - if (kind === 'image') Icon = LucideImage - else if (kind === 'video') Icon = LucideVideo + if (kind === 'image') Icon = Image + else if (kind === 'video') Icon = Video else if (kind === 'pdf') Icon = FileText const sz = compact ? 12 : 14 return ( @@ -478,13 +474,12 @@ export default function MediaLibraryPage() { const selCount = selected.size const onBulkArchiveFiles = async (e) => { - const input = e.target - const fl = input.files + const fl = e.target.files if (!fl?.length) return const list = Array.from(fl) + e.target.value = '' if (uploadVis === 'club' && !Number(uploadClubId)) { window.alert('Bitte einen Verein für die Sichtbarkeit „Verein“ wählen.') - input.value = '' return } setUploadBusy(true) @@ -507,7 +502,6 @@ export default function MediaLibraryPage() { } catch (err) { window.alert(err.message || String(err)) } finally { - input.value = '' setUploadBusy(false) } } @@ -630,7 +624,7 @@ export default function MediaLibraryPage() { ref={bulkFileInputRef} type="file" className="media-library__sr-file" - accept={MEDIA_UPLOAD_ACCEPT} + accept="image/*,video/*,application/pdf" multiple onChange={onBulkArchiveFiles} />