This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/content_feed.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

1049 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BreakPilot Studio - Content Feed Modul
Funktionen:
- Educational Content durchsuchen & entdecken
- Matrix Feed Integration
- Search & Filter (Kategorie, Alter, Lizenz, Tags)
- Content Preview & Download
- Rating System (5 Sterne + Kommentare)
- Favoriten & Collections
"""
class ContentFeedModule:
"""Modul für Content Feed & Discovery."""
@staticmethod
def get_css() -> str:
"""CSS für das Content Feed Modul."""
return """
/* =============================================
CONTENT FEED MODULE
============================================= */
.panel-content-feed {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow-y: auto;
}
/* Header with Search */
.content-feed-header {
padding: 24px 40px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
position: sticky;
top: 0;
z-index: 10;
}
.feed-header-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.feed-header-top h2 {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
}
/* Search Bar */
.feed-search-bar {
position: relative;
}
.feed-search-input {
width: 100%;
padding: 14px 48px 14px 20px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 999px;
color: var(--bp-text);
font-size: 15px;
transition: all 0.2s;
}
.feed-search-input:focus {
outline: none;
border-color: var(--bp-primary);
box-shadow: 0 0 0 3px var(--bp-primary-soft);
}
.feed-search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: var(--bp-text-muted);
font-size: 18px;
}
/* Filters */
.feed-filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
}
.filter-group {
display: flex;
gap: 8px;
align-items: center;
}
.filter-label {
font-size: 13px;
font-weight: 500;
color: var(--bp-text-muted);
}
.filter-select {
padding: 8px 16px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.filter-select:hover {
border-color: var(--bp-primary);
}
.filter-chip {
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;
}
.filter-chip:hover {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
}
.filter-chip.active {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
/* View Toggle */
.view-toggle {
display: flex;
gap: 4px;
padding: 4px;
background: var(--bp-bg);
border-radius: 8px;
}
.view-btn {
padding: 8px 12px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--bp-text-muted);
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
background: var(--bp-surface-elevated);
}
.view-btn.active {
background: var(--bp-primary);
color: white;
}
/* Content Area */
.content-feed-content {
padding: 32px 40px;
flex: 1;
}
/* Feed Grid View */
.feed-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 24px;
}
/* Feed List View */
.feed-list {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Feed Card (Grid View) */
.feed-card {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
}
.feed-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
border-color: var(--bp-primary);
}
.feed-card-thumbnail {
width: 100%;
height: 200px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
color: white;
position: relative;
}
.feed-card-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
border-radius: 6px;
font-size: 11px;
font-weight: 600;
color: white;
text-transform: uppercase;
}
.feed-card-body {
padding: 20px;
}
.feed-card-title {
font-size: 17px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
line-height: 1.4;
}
.feed-card-description {
font-size: 14px;
color: var(--bp-text-muted);
margin-bottom: 16px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.feed-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.feed-meta-badge {
padding: 4px 10px;
background: var(--bp-bg);
border-radius: 4px;
font-size: 11px;
color: var(--bp-text-muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.feed-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16px;
border-top: 1px solid var(--bp-border);
}
.feed-rating {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--bp-text-muted);
}
.feed-stars {
color: var(--bp-gold);
}
.feed-actions {
display: flex;
gap: 8px;
}
.feed-action-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--bp-border);
border-radius: 6px;
color: var(--bp-text);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.feed-action-btn:hover {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.feed-action-btn-primary {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.feed-action-btn-primary:hover {
background: var(--bp-primary-hover);
}
/* Feed List Card */
.feed-list-card {
display: flex;
gap: 20px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
cursor: pointer;
}
.feed-list-card:hover {
border-color: var(--bp-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.feed-list-thumbnail {
width: 120px;
height: 120px;
flex-shrink: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
}
.feed-list-body {
flex: 1;
}
/* Content Modal */
.content-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.content-modal {
background: var(--bp-surface);
border-radius: 16px;
max-width: 900px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.content-modal-header {
padding: 32px;
border-bottom: 1px solid var(--bp-border);
position: sticky;
top: 0;
background: var(--bp-surface);
z-index: 1;
}
.modal-close {
float: right;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--bp-border);
border-radius: 6px;
color: var(--bp-text-muted);
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.modal-close:hover {
background: var(--bp-danger);
border-color: var(--bp-danger);
color: white;
}
.content-modal-body {
padding: 32px;
}
.modal-content-preview {
width: 100%;
max-height: 500px;
background: var(--bp-bg);
border-radius: 12px;
margin-bottom: 24px;
overflow: hidden;
}
.modal-content-preview iframe,
.modal-content-preview video,
.modal-content-preview img {
width: 100%;
height: 100%;
border: none;
}
.modal-content-info {
margin-top: 24px;
}
.modal-section-title {
font-size: 14px;
font-weight: 600;
color: var(--bp-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.license-info {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
margin-bottom: 24px;
}
.license-badge-large {
font-size: 32px;
}
.license-text {
flex: 1;
}
.license-name-large {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.license-link {
font-size: 13px;
color: var(--bp-primary);
text-decoration: none;
}
.license-link:hover {
text-decoration: underline;
}
/* Rating Section */
.rating-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--bp-border);
}
.rating-stars-interactive {
display: flex;
gap: 8px;
font-size: 32px;
margin-bottom: 16px;
}
.star-btn {
background: none;
border: none;
color: var(--bp-border);
cursor: pointer;
transition: all 0.2s;
padding: 0;
}
.star-btn:hover,
.star-btn.active {
color: var(--bp-gold);
transform: scale(1.1);
}
.rating-comment {
width: 100%;
padding: 12px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text);
font-family: inherit;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
.rating-submit {
margin-top: 12px;
padding: 12px 24px;
background: var(--bp-primary);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.rating-submit:hover {
background: var(--bp-primary-hover);
}
/* Empty State */
.feed-empty {
text-align: center;
padding: 80px 20px;
color: var(--bp-text-muted);
}
.feed-empty-icon {
font-size: 80px;
margin-bottom: 24px;
}
.feed-empty-title {
font-size: 20px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 12px;
}
.feed-empty-text {
font-size: 15px;
line-height: 1.6;
}
/* Loading State */
.feed-loading {
text-align: center;
padding: 60px 20px;
color: var(--bp-text-muted);
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid var(--bp-border);
border-top-color: var(--bp-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
"""
@staticmethod
def get_html() -> str:
"""HTML für das Content Feed Modul."""
return """
<!-- Content Feed Panel -->
<div id="panel-content-feed" class="panel panel-content-feed" style="display: none;">
<!-- Header with Search -->
<div class="content-feed-header">
<div class="feed-header-top">
<h2>📚 Content Feed</h2>
<div class="view-toggle">
<button class="view-btn active" data-view="grid">⬜</button>
<button class="view-btn" data-view="list">☰</button>
</div>
</div>
<div class="feed-search-bar">
<input type="text" class="feed-search-input" id="feed-search" placeholder="Suche nach Educational Content...">
<span class="feed-search-icon">🔍</span>
</div>
<div class="feed-filters">
<div class="filter-group">
<span class="filter-label">Kategorie:</span>
<select class="filter-select" id="filter-category">
<option value="">Alle</option>
<option value="movement">🏃 Bewegung</option>
<option value="math">🔢 Mathe</option>
<option value="steam">🔬 STEAM</option>
<option value="language">📖 Sprache</option>
<option value="arts">🎨 Kunst</option>
<option value="social">🤝 Sozial</option>
<option value="mindfulness">🧘 Achtsamkeit</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Typ:</span>
<select class="filter-select" id="filter-type">
<option value="">Alle</option>
<option value="video">📹 Video</option>
<option value="pdf">📄 PDF</option>
<option value="h5p">🎓 H5P</option>
<option value="image_gallery">🖼️ Bilder</option>
<option value="audio">🎵 Audio</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Alter:</span>
<select class="filter-select" id="filter-age">
<option value="">Alle</option>
<option value="3-6">3-6 Jahre</option>
<option value="6-10">6-10 Jahre</option>
<option value="10-14">10-14 Jahre</option>
<option value="14-18">14-18 Jahre</option>
</select>
</div>
<div class="filter-chip active" data-filter="all">
Alle
</div>
<div class="filter-chip" data-filter="favorites">
⭐ Favoriten
</div>
<div class="filter-chip" data-filter="recent">
🆕 Neu
</div>
</div>
</div>
<!-- Content Area -->
<div class="content-feed-content">
<!-- Loading State -->
<div class="feed-loading" id="feed-loading">
<div class="spinner"></div>
<div>Content wird geladen...</div>
</div>
<!-- Grid View -->
<div class="feed-grid" id="feed-grid" style="display: none;">
<!-- Content cards will be inserted here -->
</div>
<!-- List View -->
<div class="feed-list" id="feed-list" style="display: none;">
<!-- Content list items will be inserted here -->
</div>
<!-- Empty State -->
<div class="feed-empty" id="feed-empty" style="display: none;">
<div class="feed-empty-icon">📦</div>
<div class="feed-empty-title">Kein Content gefunden</div>
<div class="feed-empty-text">
Versuchen Sie andere Suchbegriffe oder Filter.<br>
Oder erstellen Sie selbst Educational Content!
</div>
</div>
</div>
</div>
<!-- Content Modal Template -->
<template id="content-modal-template">
<div class="content-modal-overlay">
<div class="content-modal">
<div class="content-modal-header">
<button class="modal-close">×</button>
<h2 class="modal-title">Content Title</h2>
</div>
<div class="content-modal-body">
<div class="modal-content-preview">
<!-- Preview will be inserted here -->
</div>
<div class="modal-content-info">
<p class="modal-description">Description...</p>
<div class="feed-card-meta" style="margin: 16px 0;">
<!-- Meta badges -->
</div>
<div class="modal-section-title">Creative Commons Lizenz</div>
<div class="license-info">
<div class="license-badge-large">⚖️</div>
<div class="license-text">
<div class="license-name-large">CC BY-SA 4.0</div>
<a href="#" class="license-link" target="_blank">Details anzeigen →</a>
</div>
</div>
<div class="rating-section">
<div class="modal-section-title">Bewerten Sie diesen Content</div>
<div class="rating-stars-interactive" data-rating="0">
<button class="star-btn" data-star="1">★</button>
<button class="star-btn" data-star="2">★</button>
<button class="star-btn" data-star="3">★</button>
<button class="star-btn" data-star="4">★</button>
<button class="star-btn" data-star="5">★</button>
</div>
<textarea class="rating-comment" placeholder="Teilen Sie Ihre Erfahrungen mit diesem Content..."></textarea>
<button class="rating-submit">Bewertung abgeben</button>
</div>
<div class="form-actions" style="margin-top: 24px;">
<button class="btn-primary">📥 Content herunterladen</button>
<button class="btn-secondary">⭐ Zu Favoriten</button>
</div>
</div>
</div>
</div>
</div>
</template>
"""
@staticmethod
def get_js() -> str:
"""JavaScript für das Content Feed Modul."""
return """
// =============================================
// CONTENT FEED MODULE
// =============================================
(function() {
'use strict';
const ContentFeed = {
currentView: 'grid',
allContent: [],
filteredContent: [],
currentFilters: {
search: '',
category: '',
type: '',
age: '',
preset: 'all'
},
init() {
this.bindViewToggle();
this.bindSearch();
this.bindFilters();
this.loadContent();
},
bindViewToggle() {
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentView = view;
this.renderContent();
});
});
},
bindSearch() {
const searchInput = document.getElementById('feed-search');
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.currentFilters.search = e.target.value.toLowerCase();
this.filterContent();
}, 300);
});
},
bindFilters() {
// Select filters
['category', 'type', 'age'].forEach(filter => {
document.getElementById(`filter-${filter}`).addEventListener('change', (e) => {
this.currentFilters[filter] = e.target.value;
this.filterContent();
});
});
// Chip filters
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
this.currentFilters.preset = chip.dataset.filter;
this.filterContent();
});
});
},
async loadContent() {
document.getElementById('feed-loading').style.display = 'block';
document.getElementById('feed-grid').style.display = 'none';
document.getElementById('feed-list').style.display = 'none';
document.getElementById('feed-empty').style.display = 'none';
try {
const response = await fetch('http://localhost:8002/api/v1/content?limit=50');
if (!response.ok) throw new Error('Failed to load content');
const data = await response.json();
this.allContent = data.content || [];
this.filteredContent = this.allContent;
this.renderContent();
} catch (error) {
console.error('Error loading content:', error);
document.getElementById('feed-loading').innerHTML = `
<div style="color: var(--bp-danger);">
❌ Fehler beim Laden des Contents<br>
<small>${error.message}</small>
</div>
`;
}
},
filterContent() {
let filtered = this.allContent;
// Search filter
if (this.currentFilters.search) {
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(this.currentFilters.search) ||
(item.description && item.description.toLowerCase().includes(this.currentFilters.search))
);
}
// Category filter
if (this.currentFilters.category) {
filtered = filtered.filter(item => item.category === this.currentFilters.category);
}
// Type filter
if (this.currentFilters.type) {
filtered = filtered.filter(item => item.content_type === this.currentFilters.type);
}
// Age filter
if (this.currentFilters.age) {
const [min, max] = this.currentFilters.age.split('-').map(Number);
filtered = filtered.filter(item =>
item.age_min <= max && item.age_max >= min
);
}
// Preset filters
if (this.currentFilters.preset === 'favorites') {
// TODO: Filter favorites from localStorage
} else if (this.currentFilters.preset === 'recent') {
filtered = filtered.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
).slice(0, 20);
}
this.filteredContent = filtered;
this.renderContent();
},
renderContent() {
document.getElementById('feed-loading').style.display = 'none';
if (this.filteredContent.length === 0) {
document.getElementById('feed-grid').style.display = 'none';
document.getElementById('feed-list').style.display = 'none';
document.getElementById('feed-empty').style.display = 'block';
return;
}
document.getElementById('feed-empty').style.display = 'none';
if (this.currentView === 'grid') {
this.renderGridView();
} else {
this.renderListView();
}
},
renderGridView() {
const grid = document.getElementById('feed-grid');
grid.style.display = 'grid';
document.getElementById('feed-list').style.display = 'none';
grid.innerHTML = this.filteredContent.map(item => `
<div class="feed-card" data-id="${item.id}">
<div class="feed-card-thumbnail">
${this.getContentIcon(item.content_type)}
<div class="feed-card-badge">${item.content_type}</div>
</div>
<div class="feed-card-body">
<div class="feed-card-title">${item.title}</div>
<div class="feed-card-description">${item.description || 'Keine Beschreibung'}</div>
<div class="feed-card-meta">
<span class="feed-meta-badge">${this.getCategoryIcon(item.category)} ${this.getCategoryName(item.category)}</span>
<span class="feed-meta-badge">👥 ${item.age_min}-${item.age_max} Jahre</span>
<span class="feed-meta-badge">⚖️ ${item.license}</span>
</div>
<div class="feed-card-footer">
<div class="feed-rating">
<span class="feed-stars">⭐⭐⭐⭐⭐</span>
<span>${item.avg_rating || 0}/5</span>
</div>
<div class="feed-actions">
<button class="feed-action-btn feed-action-btn-primary" onclick="ContentFeed.openModal('${item.id}')">
Ansehen
</button>
</div>
</div>
</div>
</div>
`).join('');
},
renderListView() {
const list = document.getElementById('feed-list');
list.style.display = 'flex';
document.getElementById('feed-grid').style.display = 'none';
list.innerHTML = this.filteredContent.map(item => `
<div class="feed-list-card" data-id="${item.id}" onclick="ContentFeed.openModal('${item.id}')">
<div class="feed-list-thumbnail">
${this.getContentIcon(item.content_type)}
</div>
<div class="feed-list-body">
<div class="feed-card-title">${item.title}</div>
<div class="feed-card-description">${item.description || 'Keine Beschreibung'}</div>
<div class="feed-card-meta">
<span class="feed-meta-badge">${this.getCategoryIcon(item.category)} ${this.getCategoryName(item.category)}</span>
<span class="feed-meta-badge">👥 ${item.age_min}-${item.age_max} Jahre</span>
<span class="feed-meta-badge">⚖️ ${item.license}</span>
<span class="feed-meta-badge">📥 ${item.downloads || 0} Downloads</span>
</div>
</div>
</div>
`).join('');
},
openModal(contentId) {
const content = this.filteredContent.find(c => c.id === contentId);
if (!content) return;
const template = document.getElementById('content-modal-template');
const modal = template.content.cloneNode(true);
// Fill modal with content data
modal.querySelector('.modal-title').textContent = content.title;
modal.querySelector('.modal-description').textContent = content.description || 'Keine Beschreibung';
// Close button
modal.querySelector('.modal-close').addEventListener('click', (e) => {
e.target.closest('.content-modal-overlay').remove();
});
// Close on overlay click
modal.querySelector('.content-modal-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('content-modal-overlay')) {
e.target.remove();
}
});
// Add modal to body
document.body.appendChild(modal);
},
getContentIcon(type) {
const icons = {
video: '📹',
pdf: '📄',
h5p: '🎓',
image_gallery: '🖼️',
audio: '🎵',
markdown: '📝'
};
return icons[type] || '📦';
},
getCategoryIcon(category) {
const icons = {
movement: '🏃',
math: '🔢',
steam: '🔬',
language: '📖',
arts: '🎨',
social: '🤝',
mindfulness: '🧘'
};
return icons[category] || '📚';
},
getCategoryName(category) {
const names = {
movement: 'Bewegung',
math: 'Mathe',
steam: 'STEAM',
language: 'Sprache',
arts: 'Kunst',
social: 'Sozial',
mindfulness: 'Achtsamkeit'
};
return names[category] || category;
}
};
// Make ContentFeed globally accessible
window.ContentFeed = ContentFeed;
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ContentFeed.init());
} else {
ContentFeed.init();
}
})();
"""