A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1135 lines
29 KiB
Python
1135 lines
29 KiB
Python
"""
|
||
BreakPilot Studio - Content Creator Modul
|
||
|
||
Funktionen:
|
||
- Educational Content erstellen & verwalten
|
||
- H5P Interactive Content Editor Integration
|
||
- Creative Commons Lizenzierung
|
||
- Upload von Videos, PDFs, Bildern, Audio
|
||
- Publishing zu Matrix Feed
|
||
- Analytics & Impact Scoring
|
||
"""
|
||
|
||
|
||
class ContentCreatorModule:
|
||
"""Modul für Content Creation."""
|
||
|
||
@staticmethod
|
||
def get_css() -> str:
|
||
"""CSS für das Content Creator Modul."""
|
||
return """
|
||
/* =============================================
|
||
CONTENT CREATOR MODULE
|
||
============================================= */
|
||
|
||
.panel-content-creator {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background: var(--bp-bg);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Header */
|
||
.content-creator-header {
|
||
padding: 32px 40px 24px;
|
||
background: var(--bp-surface);
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.content-creator-header h2 {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--bp-text);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.content-creator-header p {
|
||
font-size: 14px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
/* Navigation Tabs */
|
||
.content-creator-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.creator-tab {
|
||
padding: 10px 20px;
|
||
background: transparent;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
color: var(--bp-text-muted);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.creator-tab:hover {
|
||
background: var(--bp-surface-elevated);
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.creator-tab.active {
|
||
background: var(--bp-primary);
|
||
border-color: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
/* Content Area */
|
||
.content-creator-content {
|
||
padding: 32px 40px;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Tab Panels */
|
||
.creator-tab-panel {
|
||
display: none;
|
||
}
|
||
|
||
.creator-tab-panel.active {
|
||
display: block;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Create Content Form */
|
||
.create-content-form {
|
||
max-width: 800px;
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.form-section-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
margin-bottom: 16px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--bp-text);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-label-required::after {
|
||
content: ' *';
|
||
color: var(--bp-danger);
|
||
}
|
||
|
||
.form-input,
|
||
.form-select,
|
||
.form-textarea {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
color: var(--bp-text);
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.form-input:focus,
|
||
.form-select:focus,
|
||
.form-textarea:focus {
|
||
outline: none;
|
||
border-color: var(--bp-primary);
|
||
box-shadow: 0 0 0 3px var(--bp-primary-soft);
|
||
}
|
||
|
||
.form-textarea {
|
||
min-height: 100px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 6px;
|
||
}
|
||
|
||
/* File Upload Area */
|
||
.file-upload-area {
|
||
border: 2px dashed var(--bp-border);
|
||
border-radius: 12px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
background: var(--bp-bg);
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.file-upload-area:hover {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.file-upload-area.dragover {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 16px;
|
||
color: var(--bp-text);
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.file-input-hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* Uploaded Files List */
|
||
.uploaded-files {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.uploaded-file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 24px;
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.file-remove {
|
||
padding: 6px 12px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--bp-danger);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.file-remove:hover {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* License Selector */
|
||
.license-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.license-option {
|
||
padding: 16px;
|
||
background: var(--bp-bg);
|
||
border: 2px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
}
|
||
|
||
.license-option:hover {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.license-option.selected {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.license-badge {
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.license-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.license-desc {
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
/* Category Pills */
|
||
.category-pills {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.category-pill {
|
||
padding: 8px 16px;
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 999px;
|
||
font-size: 13px;
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.category-pill:hover {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.category-pill.selected {
|
||
background: var(--bp-primary);
|
||
border-color: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
/* Tags Input */
|
||
.tags-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding: 8px;
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
min-height: 42px;
|
||
}
|
||
|
||
.tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
background: var(--bp-primary-soft);
|
||
border: 1px solid var(--bp-primary);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.tag-remove {
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.tag-remove:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.tag-input {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text);
|
||
font-size: 14px;
|
||
outline: none;
|
||
}
|
||
|
||
/* H5P Integration */
|
||
.h5p-editor-container {
|
||
margin-top: 16px;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: white;
|
||
}
|
||
|
||
.h5p-editor-frame {
|
||
width: 100%;
|
||
min-height: 600px;
|
||
border: none;
|
||
}
|
||
|
||
/* My Content Grid */
|
||
.my-content-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 24px;
|
||
}
|
||
|
||
.content-card {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.content-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.content-thumbnail {
|
||
width: 100%;
|
||
height: 180px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 48px;
|
||
color: white;
|
||
}
|
||
|
||
.content-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.content-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.content-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.meta-badge {
|
||
padding: 4px 10px;
|
||
background: var(--bp-bg);
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.content-stats {
|
||
display: flex;
|
||
gap: 16px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--bp-border);
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.stat {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.content-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.btn-icon {
|
||
padding: 8px;
|
||
background: transparent;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 6px;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-icon:hover {
|
||
background: var(--bp-primary);
|
||
border-color: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
/* Analytics Dashboard */
|
||
.analytics-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.analytics-card {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.analytics-label {
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.analytics-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: var(--bp-text);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.analytics-change {
|
||
font-size: 12px;
|
||
color: var(--bp-success);
|
||
}
|
||
|
||
.analytics-change.negative {
|
||
color: var(--bp-danger);
|
||
}
|
||
|
||
/* Action Buttons */
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 32px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.btn-primary {
|
||
flex: 1;
|
||
padding: 14px 24px;
|
||
background: var(--bp-primary);
|
||
border: none;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--bp-primary-hover);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.3);
|
||
}
|
||
|
||
.btn-secondary {
|
||
padding: 14px 24px;
|
||
background: transparent;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
color: var(--bp-text);
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--bp-surface-elevated);
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
/* Status Badge */
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-draft {
|
||
background: rgba(100, 116, 139, 0.15);
|
||
color: #64748b;
|
||
}
|
||
|
||
.status-published {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
color: #22c55e;
|
||
}
|
||
|
||
.status-review {
|
||
background: rgba(245, 158, 11, 0.15);
|
||
color: #f59e0b;
|
||
}
|
||
"""
|
||
|
||
@staticmethod
|
||
def get_html() -> str:
|
||
"""HTML für das Content Creator Modul."""
|
||
return """
|
||
<!-- Content Creator Panel -->
|
||
<div id="panel-content-creator" class="panel panel-content-creator" style="display: none;">
|
||
<!-- Header -->
|
||
<div class="content-creator-header">
|
||
<h2>🎓 Content Creator</h2>
|
||
<p>Erstellen Sie Educational Content mit Creative Commons Lizenzierung</p>
|
||
|
||
<!-- Tabs -->
|
||
<div class="content-creator-tabs">
|
||
<button class="creator-tab active" data-tab="create">
|
||
✨ Neuer Content
|
||
</button>
|
||
<button class="creator-tab" data-tab="my-content">
|
||
📚 Mein Content
|
||
</button>
|
||
<button class="creator-tab" data-tab="analytics">
|
||
📊 Analytics
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Content Area -->
|
||
<div class="content-creator-content">
|
||
|
||
<!-- Tab: Create Content -->
|
||
<div id="tab-create" class="creator-tab-panel active">
|
||
<form class="create-content-form" id="create-content-form">
|
||
|
||
<!-- Basic Info -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">📝 Grundinformationen</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label form-label-required">Titel</label>
|
||
<input type="text" class="form-input" id="content-title" placeholder="z.B. 5-Minuten Yoga für Grundschule" required>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label form-label-required">Beschreibung</label>
|
||
<textarea class="form-textarea" id="content-description" placeholder="Beschreiben Sie Ihren Content..." required></textarea>
|
||
<div class="form-hint">Was lernen Schüler mit diesem Content?</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label form-label-required">Content-Typ</label>
|
||
<select class="form-select" id="content-type" required>
|
||
<option value="">Bitte wählen...</option>
|
||
<option value="video">📹 Video</option>
|
||
<option value="pdf">📄 PDF Dokument</option>
|
||
<option value="image_gallery">🖼️ Bildergalerie</option>
|
||
<option value="audio">🎵 Audio</option>
|
||
<option value="markdown">📝 Markdown</option>
|
||
<option value="h5p">🎓 H5P Interactive</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- File Upload -->
|
||
<div class="form-section" id="file-upload-section">
|
||
<div class="form-section-title">📁 Dateien hochladen</div>
|
||
|
||
<div class="file-upload-area" id="file-upload-area">
|
||
<div class="upload-icon">☁️</div>
|
||
<div class="upload-text">Dateien hierher ziehen oder klicken zum Auswählen</div>
|
||
<div class="upload-hint">Unterstützt: MP4, PDF, JPG, PNG, MP3 (max. 100MB)</div>
|
||
<input type="file" class="file-input-hidden" id="file-input" multiple>
|
||
</div>
|
||
|
||
<div class="uploaded-files" id="uploaded-files"></div>
|
||
</div>
|
||
|
||
<!-- H5P Editor (shown only when content-type = h5p) -->
|
||
<div class="form-section" id="h5p-section" style="display: none;">
|
||
<div class="form-section-title">🎓 H5P Interactive Content</div>
|
||
<div class="h5p-editor-container">
|
||
<iframe class="h5p-editor-frame" id="h5p-editor" src="http://localhost:8003/h5p/editor/new"></iframe>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Category & Tags -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">🏷️ Kategorisierung</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label form-label-required">Kategorie</label>
|
||
<div class="category-pills" id="category-pills">
|
||
<div class="category-pill" data-category="movement">🏃 Bewegung</div>
|
||
<div class="category-pill" data-category="math">🔢 Mathe</div>
|
||
<div class="category-pill" data-category="steam">🔬 STEAM</div>
|
||
<div class="category-pill" data-category="language">📖 Sprache</div>
|
||
<div class="category-pill" data-category="arts">🎨 Kunst</div>
|
||
<div class="category-pill" data-category="social">🤝 Sozial</div>
|
||
<div class="category-pill" data-category="mindfulness">🧘 Achtsamkeit</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Altersgruppe</label>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||
<div>
|
||
<label class="form-label" style="font-size: 12px;">Von (Jahre)</label>
|
||
<input type="number" class="form-input" id="age-min" value="6" min="3" max="18">
|
||
</div>
|
||
<div>
|
||
<label class="form-label" style="font-size: 12px;">Bis (Jahre)</label>
|
||
<input type="number" class="form-input" id="age-max" value="10" min="3" max="18">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Tags</label>
|
||
<div class="tags-container" id="tags-container">
|
||
<input type="text" class="tag-input" id="tag-input" placeholder="Tag hinzufügen...">
|
||
</div>
|
||
<div class="form-hint">Enter drücken um Tags hinzuzufügen</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- License -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">⚖️ Creative Commons Lizenz</div>
|
||
|
||
<div class="license-options" id="license-options">
|
||
<div class="license-option" data-license="CC-BY-SA-4.0">
|
||
<div class="license-badge">🔄</div>
|
||
<div class="license-name">CC BY-SA</div>
|
||
<div class="license-desc">Empfohlen</div>
|
||
</div>
|
||
<div class="license-option" data-license="CC-BY-4.0">
|
||
<div class="license-badge">✅</div>
|
||
<div class="license-name">CC BY</div>
|
||
<div class="license-desc">Attribution</div>
|
||
</div>
|
||
<div class="license-option" data-license="CC-BY-NC-4.0">
|
||
<div class="license-badge">🚫</div>
|
||
<div class="license-name">CC BY-NC</div>
|
||
<div class="license-desc">Nicht-kommerziell</div>
|
||
</div>
|
||
<div class="license-option" data-license="CC0-1.0">
|
||
<div class="license-badge">🌍</div>
|
||
<div class="license-name">CC0</div>
|
||
<div class="license-desc">Public Domain</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="form-actions">
|
||
<button type="button" class="btn-secondary" id="btn-save-draft">💾 Als Entwurf speichern</button>
|
||
<button type="submit" class="btn-primary">🚀 Content veröffentlichen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Tab: My Content -->
|
||
<div id="tab-my-content" class="creator-tab-panel">
|
||
<div class="my-content-grid" id="my-content-grid">
|
||
<!-- Content wird hier dynamisch eingefügt -->
|
||
<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--bp-text-muted);">
|
||
<div style="font-size: 64px; margin-bottom: 16px;">📦</div>
|
||
<div style="font-size: 16px; margin-bottom: 8px;">Noch kein Content erstellt</div>
|
||
<div style="font-size: 14px;">Erstellen Sie Ihren ersten Educational Content!</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Analytics -->
|
||
<div id="tab-analytics" class="creator-tab-panel">
|
||
<div class="analytics-grid">
|
||
<div class="analytics-card">
|
||
<div class="analytics-label">Gesamt Downloads</div>
|
||
<div class="analytics-value" id="total-downloads">0</div>
|
||
<div class="analytics-change">+0% diese Woche</div>
|
||
</div>
|
||
<div class="analytics-card">
|
||
<div class="analytics-label">Durchschn. Rating</div>
|
||
<div class="analytics-value" id="avg-rating">0.0</div>
|
||
<div class="analytics-change">⭐⭐⭐⭐⭐</div>
|
||
</div>
|
||
<div class="analytics-card">
|
||
<div class="analytics-label">Impact Score</div>
|
||
<div class="analytics-value" id="impact-score">0</div>
|
||
<div class="analytics-change">+0 Punkte</div>
|
||
</div>
|
||
<div class="analytics-card">
|
||
<div class="analytics-label">Veröffentlicht</div>
|
||
<div class="analytics-value" id="published-count">0</div>
|
||
<div class="analytics-change">Content-Stücke</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="text-align: center; padding: 60px; color: var(--bp-text-muted);">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📊</div>
|
||
<div style="font-size: 16px;">Detaillierte Analytics kommen bald!</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
@staticmethod
|
||
def get_js() -> str:
|
||
"""JavaScript für das Content Creator Modul."""
|
||
return """
|
||
// =============================================
|
||
// CONTENT CREATOR MODULE
|
||
// =============================================
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
const ContentCreator = {
|
||
selectedFiles: [],
|
||
selectedCategory: null,
|
||
selectedLicense: 'CC-BY-SA-4.0',
|
||
tags: [],
|
||
|
||
init() {
|
||
this.bindTabSwitching();
|
||
this.bindFileUpload();
|
||
this.bindCategorySelection();
|
||
this.bindLicenseSelection();
|
||
this.bindTagsInput();
|
||
this.bindContentTypeChange();
|
||
this.bindFormSubmit();
|
||
|
||
// Set default license
|
||
document.querySelector(`[data-license="${this.selectedLicense}"]`)?.classList.add('selected');
|
||
},
|
||
|
||
bindTabSwitching() {
|
||
const tabs = document.querySelectorAll('.creator-tab');
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabName = tab.dataset.tab;
|
||
|
||
// Update tabs
|
||
tabs.forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
|
||
// Update panels
|
||
document.querySelectorAll('.creator-tab-panel').forEach(panel => {
|
||
panel.classList.remove('active');
|
||
});
|
||
document.getElementById(`tab-${tabName}`)?.classList.add('active');
|
||
|
||
// Load content if switching to my-content
|
||
if (tabName === 'my-content') {
|
||
this.loadMyContent();
|
||
} else if (tabName === 'analytics') {
|
||
this.loadAnalytics();
|
||
}
|
||
});
|
||
});
|
||
},
|
||
|
||
bindFileUpload() {
|
||
const uploadArea = document.getElementById('file-upload-area');
|
||
const fileInput = document.getElementById('file-input');
|
||
|
||
uploadArea.addEventListener('click', () => fileInput.click());
|
||
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
this.handleFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
this.handleFiles(e.target.files);
|
||
});
|
||
},
|
||
|
||
handleFiles(files) {
|
||
Array.from(files).forEach(file => {
|
||
if (file.size > 100 * 1024 * 1024) {
|
||
alert(`Datei ${file.name} ist zu groß (max. 100MB)`);
|
||
return;
|
||
}
|
||
|
||
this.selectedFiles.push(file);
|
||
this.renderUploadedFiles();
|
||
});
|
||
},
|
||
|
||
renderUploadedFiles() {
|
||
const container = document.getElementById('uploaded-files');
|
||
container.innerHTML = '';
|
||
|
||
this.selectedFiles.forEach((file, index) => {
|
||
const fileEl = document.createElement('div');
|
||
fileEl.className = 'uploaded-file';
|
||
fileEl.innerHTML = `
|
||
<div class="file-icon">${this.getFileIcon(file.type)}</div>
|
||
<div class="file-info">
|
||
<div class="file-name">${file.name}</div>
|
||
<div class="file-size">${this.formatFileSize(file.size)}</div>
|
||
</div>
|
||
<button type="button" class="file-remove" data-index="${index}">Entfernen</button>
|
||
`;
|
||
|
||
fileEl.querySelector('.file-remove').addEventListener('click', () => {
|
||
this.selectedFiles.splice(index, 1);
|
||
this.renderUploadedFiles();
|
||
});
|
||
|
||
container.appendChild(fileEl);
|
||
});
|
||
},
|
||
|
||
getFileIcon(type) {
|
||
if (type.startsWith('video/')) return '📹';
|
||
if (type.startsWith('image/')) return '🖼️';
|
||
if (type.startsWith('audio/')) return '🎵';
|
||
if (type === 'application/pdf') return '📄';
|
||
return '📁';
|
||
},
|
||
|
||
formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
},
|
||
|
||
bindCategorySelection() {
|
||
document.querySelectorAll('.category-pill').forEach(pill => {
|
||
pill.addEventListener('click', () => {
|
||
document.querySelectorAll('.category-pill').forEach(p => p.classList.remove('selected'));
|
||
pill.classList.add('selected');
|
||
this.selectedCategory = pill.dataset.category;
|
||
});
|
||
});
|
||
},
|
||
|
||
bindLicenseSelection() {
|
||
document.querySelectorAll('.license-option').forEach(option => {
|
||
option.addEventListener('click', () => {
|
||
document.querySelectorAll('.license-option').forEach(o => o.classList.remove('selected'));
|
||
option.classList.add('selected');
|
||
this.selectedLicense = option.dataset.license;
|
||
});
|
||
});
|
||
},
|
||
|
||
bindTagsInput() {
|
||
const input = document.getElementById('tag-input');
|
||
const container = document.getElementById('tags-container');
|
||
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && input.value.trim()) {
|
||
e.preventDefault();
|
||
const tag = input.value.trim();
|
||
if (!this.tags.includes(tag)) {
|
||
this.tags.push(tag);
|
||
this.renderTags();
|
||
}
|
||
input.value = '';
|
||
}
|
||
});
|
||
},
|
||
|
||
renderTags() {
|
||
const container = document.getElementById('tags-container');
|
||
const input = document.getElementById('tag-input');
|
||
|
||
// Remove old tags
|
||
container.querySelectorAll('.tag').forEach(el => el.remove());
|
||
|
||
// Add new tags
|
||
this.tags.forEach((tag, index) => {
|
||
const tagEl = document.createElement('div');
|
||
tagEl.className = 'tag';
|
||
tagEl.innerHTML = `
|
||
${tag}
|
||
<span class="tag-remove" data-index="${index}">×</span>
|
||
`;
|
||
|
||
tagEl.querySelector('.tag-remove').addEventListener('click', () => {
|
||
this.tags.splice(index, 1);
|
||
this.renderTags();
|
||
});
|
||
|
||
container.insertBefore(tagEl, input);
|
||
});
|
||
},
|
||
|
||
bindContentTypeChange() {
|
||
const typeSelect = document.getElementById('content-type');
|
||
typeSelect.addEventListener('change', () => {
|
||
const h5pSection = document.getElementById('h5p-section');
|
||
const fileSection = document.getElementById('file-upload-section');
|
||
|
||
if (typeSelect.value === 'h5p') {
|
||
h5pSection.style.display = 'block';
|
||
fileSection.style.display = 'none';
|
||
} else {
|
||
h5pSection.style.display = 'none';
|
||
fileSection.style.display = 'block';
|
||
}
|
||
});
|
||
},
|
||
|
||
bindFormSubmit() {
|
||
const form = document.getElementById('create-content-form');
|
||
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
await this.publishContent();
|
||
});
|
||
|
||
document.getElementById('btn-save-draft').addEventListener('click', () => {
|
||
this.saveDraft();
|
||
});
|
||
},
|
||
|
||
async publishContent() {
|
||
if (!this.selectedCategory) {
|
||
alert('Bitte wählen Sie eine Kategorie aus.');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
title: document.getElementById('content-title').value,
|
||
description: document.getElementById('content-description').value,
|
||
content_type: document.getElementById('content-type').value,
|
||
category: this.selectedCategory,
|
||
license: this.selectedLicense,
|
||
age_min: parseInt(document.getElementById('age-min').value),
|
||
age_max: parseInt(document.getElementById('age-max').value),
|
||
tags: this.tags,
|
||
status: 'published'
|
||
};
|
||
|
||
try {
|
||
// Create content
|
||
const response = await fetch('http://localhost:8002/api/v1/content', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to create content');
|
||
|
||
const content = await response.json();
|
||
|
||
// Upload files if any
|
||
if (this.selectedFiles.length > 0) {
|
||
await this.uploadFiles(content.id);
|
||
}
|
||
|
||
alert('✅ Content erfolgreich veröffentlicht und zu Matrix Feed hinzugefügt!');
|
||
this.resetForm();
|
||
|
||
// Switch to my-content tab
|
||
document.querySelector('[data-tab="my-content"]').click();
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('❌ Fehler beim Veröffentlichen: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async uploadFiles(contentId) {
|
||
for (const file of this.selectedFiles) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch('http://localhost:8002/api/v1/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) throw new Error('File upload failed');
|
||
|
||
const { file_url } = await response.json();
|
||
|
||
// Attach file to content
|
||
await fetch(`http://localhost:8002/api/v1/content/${contentId}/files`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ file_urls: [file_url] })
|
||
});
|
||
}
|
||
},
|
||
|
||
saveDraft() {
|
||
alert('💾 Als Entwurf gespeichert (Feature kommt bald)');
|
||
},
|
||
|
||
resetForm() {
|
||
document.getElementById('create-content-form').reset();
|
||
this.selectedFiles = [];
|
||
this.selectedCategory = null;
|
||
this.tags = [];
|
||
this.renderUploadedFiles();
|
||
this.renderTags();
|
||
document.querySelectorAll('.category-pill').forEach(p => p.classList.remove('selected'));
|
||
},
|
||
|
||
async loadMyContent() {
|
||
// TODO: Load user's content from API
|
||
console.log('Loading my content...');
|
||
},
|
||
|
||
async loadAnalytics() {
|
||
// TODO: Load analytics from API
|
||
console.log('Loading analytics...');
|
||
}
|
||
};
|
||
|
||
// Initialize when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => ContentCreator.init());
|
||
} else {
|
||
ContentCreator.init();
|
||
}
|
||
})();
|
||
"""
|