fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
358
h5p-service/players/course-presentation-player.html
Normal file
358
h5p-service/players/course-presentation-player.html
Normal file
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Course Presentation - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #1f2937;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.presentation-container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
.slide-viewer {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
.slide {
|
||||
display: none;
|
||||
width: 100%;
|
||||
animation: slideIn 0.4s ease-out;
|
||||
}
|
||||
.slide.active {
|
||||
display: block;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.slide-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.slide-content {
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
color: #4b5563;
|
||||
margin-bottom: 24px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.slide-image {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
margin-top: 24px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
background: #f9fafb;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.progress-info {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.thumbnails {
|
||||
display: none;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background: #f9fafb;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
}
|
||||
.thumbnails.show {
|
||||
display: grid;
|
||||
}
|
||||
.thumbnail {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.thumbnail:hover {
|
||||
border-color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
.thumbnail.active {
|
||||
border-color: #667eea;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
.thumbnail-number {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.thumbnail-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="presentation-container">
|
||||
<div class="header">
|
||||
<h1 id="presentationTitle"></h1>
|
||||
<p id="presentationDescription"></p>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
|
||||
<div class="slide-viewer" id="slideViewer"></div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="nav-buttons">
|
||||
<button class="btn btn-primary" id="prevBtn" onclick="previousSlide()">
|
||||
← Zurück
|
||||
</button>
|
||||
<button class="btn btn-primary" id="nextBtn" onclick="nextSlide()">
|
||||
Weiter →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-info">
|
||||
<span id="currentSlide">1</span> / <span id="totalSlides">1</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary" onclick="toggleThumbnails()">
|
||||
📋 Übersicht
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="thumbnails" id="thumbnails"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
let currentSlideIndex = 0;
|
||||
|
||||
function loadContent() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('presentationTitle').textContent = data.title;
|
||||
document.getElementById('presentationDescription').textContent = data.description || '';
|
||||
document.getElementById('totalSlides').textContent = data.slides.length;
|
||||
|
||||
renderSlides();
|
||||
renderThumbnails();
|
||||
updateProgress();
|
||||
updateNavButtons();
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') previousSlide();
|
||||
if (e.key === 'ArrowRight') nextSlide();
|
||||
if (e.key === 'Escape') hideThumbnails();
|
||||
});
|
||||
}
|
||||
|
||||
function renderSlides() {
|
||||
const viewer = document.getElementById('slideViewer');
|
||||
viewer.innerHTML = '';
|
||||
|
||||
data.slides.forEach((slide, index) => {
|
||||
const slideEl = document.createElement('div');
|
||||
slideEl.className = 'slide';
|
||||
if (index === 0) slideEl.classList.add('active');
|
||||
|
||||
slideEl.style.backgroundColor = slide.backgroundColor;
|
||||
|
||||
slideEl.innerHTML = `
|
||||
<div class="slide-title">${slide.title}</div>
|
||||
<div class="slide-content">${slide.content}</div>
|
||||
${slide.imageUrl ? `<img src="${slide.imageUrl}" class="slide-image" alt="${slide.title}">` : ''}
|
||||
`;
|
||||
|
||||
viewer.appendChild(slideEl);
|
||||
});
|
||||
}
|
||||
|
||||
function renderThumbnails() {
|
||||
const container = document.getElementById('thumbnails');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.slides.forEach((slide, index) => {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'thumbnail';
|
||||
if (index === 0) thumb.classList.add('active');
|
||||
|
||||
thumb.innerHTML = `
|
||||
<div class="thumbnail-number">Folie ${index + 1}</div>
|
||||
<div class="thumbnail-title">${slide.title}</div>
|
||||
`;
|
||||
|
||||
thumb.onclick = () => goToSlide(index);
|
||||
container.appendChild(thumb);
|
||||
});
|
||||
}
|
||||
|
||||
function goToSlide(index) {
|
||||
if (index < 0 || index >= data.slides.length) return;
|
||||
|
||||
// Hide current slide
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
slides[currentSlideIndex].classList.remove('active');
|
||||
|
||||
// Update thumbnails
|
||||
const thumbs = document.querySelectorAll('.thumbnail');
|
||||
thumbs[currentSlideIndex].classList.remove('active');
|
||||
|
||||
// Show new slide
|
||||
currentSlideIndex = index;
|
||||
slides[currentSlideIndex].classList.add('active');
|
||||
thumbs[currentSlideIndex].classList.add('active');
|
||||
|
||||
updateProgress();
|
||||
updateNavButtons();
|
||||
hideThumbnails();
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
goToSlide(currentSlideIndex + 1);
|
||||
}
|
||||
|
||||
function previousSlide() {
|
||||
goToSlide(currentSlideIndex - 1);
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
document.getElementById('currentSlide').textContent = currentSlideIndex + 1;
|
||||
const progress = ((currentSlideIndex + 1) / data.slides.length) * 100;
|
||||
document.getElementById('progressFill').style.width = progress + '%';
|
||||
}
|
||||
|
||||
function updateNavButtons() {
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
prevBtn.disabled = currentSlideIndex === 0;
|
||||
nextBtn.disabled = currentSlideIndex === data.slides.length - 1;
|
||||
}
|
||||
|
||||
function toggleThumbnails() {
|
||||
const thumbnails = document.getElementById('thumbnails');
|
||||
thumbnails.classList.toggle('show');
|
||||
}
|
||||
|
||||
function hideThumbnails() {
|
||||
const thumbnails = document.getElementById('thumbnails');
|
||||
thumbnails.classList.remove('show');
|
||||
}
|
||||
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
439
h5p-service/players/drag-drop-player.html
Normal file
439
h5p-service/players/drag-drop-player.html
Normal file
@@ -0,0 +1,439 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Drag and Drop - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.question {
|
||||
color: #6b7280;
|
||||
margin-bottom: 32px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.game-area {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.zones-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.drop-zone {
|
||||
background: #f9fafb;
|
||||
border: 3px dashed #d1d5db;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.drop-zone.drag-over {
|
||||
background: #e0e7ff;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.drop-zone.correct {
|
||||
background: #d1fae5;
|
||||
border-color: #10b981;
|
||||
border-style: solid;
|
||||
}
|
||||
.zone-title {
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.zone-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
.draggables-container {
|
||||
background: #fef3c7;
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
.draggables-title {
|
||||
font-weight: 700;
|
||||
color: #92400e;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.draggables-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.draggable {
|
||||
background: white;
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.draggable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.draggable.dragging {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.draggable.correct {
|
||||
border-color: #10b981;
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
cursor: default;
|
||||
}
|
||||
.draggable.incorrect {
|
||||
border-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.results {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
display: none;
|
||||
}
|
||||
.results h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.results .score {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.feedback {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.feedback.show {
|
||||
display: block;
|
||||
}
|
||||
.feedback.correct {
|
||||
background: #d1fae5;
|
||||
border-color: #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
.feedback.incorrect {
|
||||
background: #fee2e2;
|
||||
border-color: #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.game-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title"></h1>
|
||||
<p class="question" id="question"></p>
|
||||
|
||||
<div class="results" id="results">
|
||||
<h2>🎉 Geschafft!</h2>
|
||||
<div class="score" id="scoreDisplay"></div>
|
||||
<p id="resultMessage"></p>
|
||||
</div>
|
||||
|
||||
<div class="feedback" id="feedback"></div>
|
||||
|
||||
<div class="game-area">
|
||||
<div class="zones-container" id="zonesContainer"></div>
|
||||
|
||||
<div class="draggables-container">
|
||||
<div class="draggables-title">🔲 Ziehe die Elemente</div>
|
||||
<div class="draggables-items" id="draggablesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="checkBtn" onclick="checkAnswers()">
|
||||
Antworten überprüfen
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="retryBtn" onclick="retry()" style="display: none;">
|
||||
Nochmal versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
let isChecked = false;
|
||||
|
||||
function loadContent() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('question').textContent = data.question || 'Ziehe die Elemente in die richtigen Zonen.';
|
||||
|
||||
renderZones();
|
||||
renderDraggables();
|
||||
setupDragAndDrop();
|
||||
}
|
||||
|
||||
function renderZones() {
|
||||
const container = document.getElementById('zonesContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.zones.forEach((zone) => {
|
||||
const zoneEl = document.createElement('div');
|
||||
zoneEl.className = 'drop-zone';
|
||||
zoneEl.dataset.zoneId = zone.id;
|
||||
zoneEl.innerHTML = `
|
||||
<div class="zone-title">${zone.name}</div>
|
||||
<div class="zone-items" data-zone-id="${zone.id}"></div>
|
||||
`;
|
||||
container.appendChild(zoneEl);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDraggables() {
|
||||
const container = document.getElementById('draggablesContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Shuffle draggables
|
||||
const shuffled = [...data.draggables].sort(() => Math.random() - 0.5);
|
||||
|
||||
shuffled.forEach((draggable) => {
|
||||
const draggableEl = document.createElement('div');
|
||||
draggableEl.className = 'draggable';
|
||||
draggableEl.draggable = true;
|
||||
draggableEl.dataset.draggableId = draggable.id;
|
||||
draggableEl.dataset.correctZoneId = draggable.correctZoneId;
|
||||
draggableEl.textContent = draggable.text;
|
||||
container.appendChild(draggableEl);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDragAndDrop() {
|
||||
// Draggable elements
|
||||
document.querySelectorAll('.draggable').forEach(draggable => {
|
||||
draggable.addEventListener('dragstart', (e) => {
|
||||
if (isChecked) return;
|
||||
draggable.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/html', draggable.outerHTML);
|
||||
});
|
||||
|
||||
draggable.addEventListener('dragend', (e) => {
|
||||
draggable.classList.remove('dragging');
|
||||
});
|
||||
});
|
||||
|
||||
// Drop zones
|
||||
document.querySelectorAll('.zone-items').forEach(zone => {
|
||||
zone.addEventListener('dragover', (e) => {
|
||||
if (isChecked) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
zone.closest('.drop-zone').classList.add('drag-over');
|
||||
});
|
||||
|
||||
zone.addEventListener('dragleave', (e) => {
|
||||
zone.closest('.drop-zone').classList.remove('drag-over');
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', (e) => {
|
||||
if (isChecked) return;
|
||||
e.preventDefault();
|
||||
zone.closest('.drop-zone').classList.remove('drag-over');
|
||||
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (dragging) {
|
||||
zone.appendChild(dragging);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Original container
|
||||
const draggablesContainer = document.getElementById('draggablesContainer');
|
||||
draggablesContainer.addEventListener('dragover', (e) => {
|
||||
if (isChecked) return;
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
draggablesContainer.addEventListener('drop', (e) => {
|
||||
if (isChecked) return;
|
||||
e.preventDefault();
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (dragging) {
|
||||
draggablesContainer.appendChild(dragging);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkAnswers() {
|
||||
if (isChecked) return;
|
||||
isChecked = true;
|
||||
|
||||
let correct = 0;
|
||||
const total = data.draggables.length;
|
||||
|
||||
data.zones.forEach(zone => {
|
||||
const zoneItems = document.querySelector(`.zone-items[data-zone-id="${zone.id}"]`);
|
||||
const draggablesInZone = zoneItems.querySelectorAll('.draggable');
|
||||
|
||||
draggablesInZone.forEach(draggable => {
|
||||
const correctZoneId = parseInt(draggable.dataset.correctZoneId);
|
||||
draggable.draggable = false;
|
||||
|
||||
if (correctZoneId === zone.id) {
|
||||
draggable.classList.add('correct');
|
||||
zoneItems.closest('.drop-zone').classList.add('correct');
|
||||
correct++;
|
||||
} else {
|
||||
draggable.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check items still in draggables container
|
||||
document.getElementById('draggablesContainer').querySelectorAll('.draggable').forEach(draggable => {
|
||||
draggable.classList.add('incorrect');
|
||||
draggable.draggable = false;
|
||||
});
|
||||
|
||||
const percentage = Math.round((correct / total) * 100);
|
||||
|
||||
// Show feedback
|
||||
const feedback = document.getElementById('feedback');
|
||||
if (correct === total) {
|
||||
feedback.className = 'feedback correct show';
|
||||
feedback.textContent = `🎉 Perfekt! Alle ${total} Elemente richtig zugeordnet!`;
|
||||
|
||||
setTimeout(() => {
|
||||
showResults(correct, total);
|
||||
}, 1500);
|
||||
} else {
|
||||
feedback.className = 'feedback incorrect show';
|
||||
feedback.textContent = `Du hast ${correct} von ${total} Elementen richtig zugeordnet (${percentage}%).`;
|
||||
}
|
||||
|
||||
document.getElementById('checkBtn').style.display = 'none';
|
||||
document.getElementById('retryBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function showResults(correct, total) {
|
||||
const percentage = Math.round((correct / total) * 100);
|
||||
|
||||
document.getElementById('scoreDisplay').textContent = `${correct} / ${total}`;
|
||||
document.getElementById('resultMessage').textContent = `${percentage}% richtig - Sehr gut!`;
|
||||
document.getElementById('results').style.display = 'block';
|
||||
}
|
||||
|
||||
function retry() {
|
||||
isChecked = false;
|
||||
|
||||
// Move all draggables back
|
||||
const draggablesContainer = document.getElementById('draggablesContainer');
|
||||
document.querySelectorAll('.draggable').forEach(draggable => {
|
||||
draggable.classList.remove('correct', 'incorrect');
|
||||
draggable.draggable = true;
|
||||
draggablesContainer.appendChild(draggable);
|
||||
});
|
||||
|
||||
// Clear zone correct state
|
||||
document.querySelectorAll('.drop-zone').forEach(zone => {
|
||||
zone.classList.remove('correct');
|
||||
});
|
||||
|
||||
document.getElementById('feedback').classList.remove('show');
|
||||
document.getElementById('results').style.display = 'none';
|
||||
|
||||
document.getElementById('checkBtn').style.display = 'inline-block';
|
||||
document.getElementById('retryBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
314
h5p-service/players/fill-blanks-player.html
Normal file
314
h5p-service/players/fill-blanks-player.html
Normal file
@@ -0,0 +1,314 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fill in the Blanks Player - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.instructions {
|
||||
color: #6b7280;
|
||||
margin-bottom: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.text-container {
|
||||
font-size: 18px;
|
||||
line-height: 2;
|
||||
color: #1f2937;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.blank-input {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
border-bottom: 2px solid #9ca3af;
|
||||
background: #f9fafb;
|
||||
padding: 4px 12px;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.blank-input:focus {
|
||||
outline: none;
|
||||
border-bottom-color: #667eea;
|
||||
background: white;
|
||||
}
|
||||
.blank-input.correct {
|
||||
border-bottom-color: #10b981;
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
.blank-input.incorrect {
|
||||
border-bottom-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.blank-input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.results {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
display: none;
|
||||
}
|
||||
.results h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.results .score {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.feedback {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.feedback.show {
|
||||
display: block;
|
||||
}
|
||||
.feedback.correct {
|
||||
background: #d1fae5;
|
||||
border-color: #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
.feedback.incorrect {
|
||||
background: #fee2e2;
|
||||
border-color: #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title"></h1>
|
||||
<p class="instructions" id="instructions"></p>
|
||||
|
||||
<div class="results" id="results">
|
||||
<h2>🎉 Fertig!</h2>
|
||||
<div class="score" id="scoreDisplay"></div>
|
||||
<p id="resultMessage"></p>
|
||||
</div>
|
||||
|
||||
<div class="feedback" id="feedback"></div>
|
||||
|
||||
<div class="text-container" id="textContainer"></div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="checkBtn" onclick="checkAnswers()">
|
||||
Antworten überprüfen
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="showBtn" onclick="showSolution()" style="display: none;">
|
||||
Lösung anzeigen
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="retryBtn" onclick="retry()" style="display: none;">
|
||||
Nochmal versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
let userAnswers = [];
|
||||
let isChecked = false;
|
||||
|
||||
function loadContent() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('instructions').textContent = data.instructions || 'Fülle die Lücken aus.';
|
||||
|
||||
renderText();
|
||||
}
|
||||
|
||||
function renderText() {
|
||||
const container = document.getElementById('textContainer');
|
||||
let text = data.text;
|
||||
let blankIndex = 0;
|
||||
|
||||
// Replace *word* with input fields
|
||||
const html = text.replace(/\*([^*]+)\*/g, (match, word) => {
|
||||
const inputId = `blank_${blankIndex}`;
|
||||
blankIndex++;
|
||||
return `<input type="text" class="blank-input" id="${inputId}" data-answer="${word}" />`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Initialize userAnswers array
|
||||
userAnswers = new Array(blankIndex).fill('');
|
||||
|
||||
// Add event listeners
|
||||
document.querySelectorAll('.blank-input').forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => {
|
||||
userAnswers[index] = e.target.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkAnswers() {
|
||||
if (isChecked) return;
|
||||
isChecked = true;
|
||||
|
||||
let correct = 0;
|
||||
const inputs = document.querySelectorAll('.blank-input');
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const correctAnswer = input.dataset.answer.toLowerCase().trim();
|
||||
const userAnswer = input.value.toLowerCase().trim();
|
||||
|
||||
input.disabled = true;
|
||||
|
||||
if (userAnswer === correctAnswer) {
|
||||
input.classList.add('correct');
|
||||
correct++;
|
||||
} else {
|
||||
input.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
const total = inputs.length;
|
||||
const percentage = Math.round((correct / total) * 100);
|
||||
|
||||
// Show feedback
|
||||
const feedback = document.getElementById('feedback');
|
||||
if (correct === total) {
|
||||
feedback.className = 'feedback correct show';
|
||||
feedback.textContent = `🎉 Perfekt! Alle ${total} Lücken richtig ausgefüllt!`;
|
||||
} else {
|
||||
feedback.className = 'feedback incorrect show';
|
||||
feedback.textContent = `Du hast ${correct} von ${total} Lücken richtig ausgefüllt (${percentage}%).`;
|
||||
}
|
||||
|
||||
// Update buttons
|
||||
document.getElementById('checkBtn').style.display = 'none';
|
||||
document.getElementById('showBtn').style.display = 'inline-block';
|
||||
document.getElementById('retryBtn').style.display = 'inline-block';
|
||||
|
||||
// Show results
|
||||
if (correct === total) {
|
||||
setTimeout(() => {
|
||||
showResults(correct, total);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
function showSolution() {
|
||||
const inputs = document.querySelectorAll('.blank-input');
|
||||
inputs.forEach((input) => {
|
||||
input.value = input.dataset.answer;
|
||||
input.classList.remove('incorrect');
|
||||
input.classList.add('correct');
|
||||
});
|
||||
|
||||
const feedback = document.getElementById('feedback');
|
||||
feedback.className = 'feedback show';
|
||||
feedback.textContent = '💡 Hier ist die vollständige Lösung.';
|
||||
}
|
||||
|
||||
function retry() {
|
||||
isChecked = false;
|
||||
userAnswers = [];
|
||||
|
||||
const inputs = document.querySelectorAll('.blank-input');
|
||||
inputs.forEach((input) => {
|
||||
input.value = '';
|
||||
input.disabled = false;
|
||||
input.classList.remove('correct', 'incorrect');
|
||||
});
|
||||
|
||||
document.getElementById('feedback').classList.remove('show');
|
||||
document.getElementById('results').style.display = 'none';
|
||||
|
||||
document.getElementById('checkBtn').style.display = 'inline-block';
|
||||
document.getElementById('showBtn').style.display = 'none';
|
||||
document.getElementById('retryBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
function showResults(correct, total) {
|
||||
const percentage = Math.round((correct / total) * 100);
|
||||
|
||||
document.getElementById('scoreDisplay').textContent = `${correct} / ${total}`;
|
||||
document.getElementById('resultMessage').textContent = `${percentage}% richtig!`;
|
||||
document.getElementById('results').style.display = 'block';
|
||||
}
|
||||
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
425
h5p-service/players/interactive-video-player.html
Normal file
425
h5p-service/players/interactive-video-player.html
Normal file
@@ -0,0 +1,425 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive Video - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.description {
|
||||
color: #6b7280;
|
||||
margin-bottom: 24px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.video-wrapper {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/9;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.video-wrapper iframe,
|
||||
.video-wrapper video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.interaction-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
.interaction-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
.interaction-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
.interaction-icon {
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.interaction-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.interaction-text {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.interaction-link {
|
||||
display: block;
|
||||
padding: 12px 24px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.interaction-link:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.timeline {
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.timeline-title {
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.timeline-markers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.timeline-marker {
|
||||
background: #fef3c7;
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #92400e;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.timeline-marker:hover {
|
||||
background: #fde68a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.timeline-marker.completed {
|
||||
background: #d1fae5;
|
||||
border-color: #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
.controls {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title"></h1>
|
||||
<p class="description" id="description"></p>
|
||||
|
||||
<div class="video-wrapper">
|
||||
<div id="videoContainer"></div>
|
||||
|
||||
<div class="interaction-overlay" id="interactionOverlay">
|
||||
<div class="interaction-content">
|
||||
<div class="interaction-icon" id="interactionIcon"></div>
|
||||
<div class="interaction-title" id="interactionTitle"></div>
|
||||
<div class="interaction-text" id="interactionText"></div>
|
||||
<div id="interactionExtra"></div>
|
||||
<button class="btn btn-primary" onclick="closeInteraction()">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="timeline-title">📌 Interaktive Elemente im Video:</div>
|
||||
<div class="timeline-markers" id="timelineMarkers"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
ℹ️ Das Video pausiert automatisch bei interaktiven Elementen
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://www.youtube.com/iframe_api"></script>
|
||||
<script src="https://player.vimeo.com/api/player.js"></script>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
let player = null;
|
||||
let playerType = null;
|
||||
let currentTime = 0;
|
||||
let completedInteractions = new Set();
|
||||
let checkInterval = null;
|
||||
|
||||
function loadContent() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('description').textContent = data.description || '';
|
||||
|
||||
renderTimeline();
|
||||
embedVideo();
|
||||
}
|
||||
|
||||
function embedVideo() {
|
||||
const container = document.getElementById('videoContainer');
|
||||
const url = data.videoUrl;
|
||||
|
||||
// YouTube
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)?.[1];
|
||||
if (videoId) {
|
||||
playerType = 'youtube';
|
||||
container.innerHTML = `<div id="ytPlayer"></div>`;
|
||||
|
||||
// YouTube API will be loaded via script tag
|
||||
window.onYouTubeIframeAPIReady = function() {
|
||||
player = new YT.Player('ytPlayer', {
|
||||
videoId: videoId,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
playerVars: {
|
||||
autoplay: 0,
|
||||
modestbranding: 1,
|
||||
rel: 0
|
||||
},
|
||||
events: {
|
||||
onReady: function() {
|
||||
startTimeTracking();
|
||||
},
|
||||
onStateChange: function(event) {
|
||||
if (event.data === YT.PlayerState.PLAYING) {
|
||||
startTimeTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
// Vimeo
|
||||
else if (url.includes('vimeo.com')) {
|
||||
const videoId = url.match(/vimeo\.com\/(\d+)/)?.[1];
|
||||
if (videoId) {
|
||||
playerType = 'vimeo';
|
||||
container.innerHTML = `<iframe id="vimeoPlayer" src="https://player.vimeo.com/video/${videoId}" allowfullscreen></iframe>`;
|
||||
|
||||
setTimeout(() => {
|
||||
player = new Vimeo.Player('vimeoPlayer');
|
||||
player.on('timeupdate', function(data) {
|
||||
currentTime = data.seconds;
|
||||
checkInteractions();
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
// Direct MP4
|
||||
else if (url.endsWith('.mp4')) {
|
||||
playerType = 'html5';
|
||||
container.innerHTML = `<video id="html5Player" controls style="width: 100%; height: 100%;"><source src="${url}" type="video/mp4"></video>`;
|
||||
|
||||
const video = document.getElementById('html5Player');
|
||||
video.addEventListener('timeupdate', function() {
|
||||
currentTime = video.currentTime;
|
||||
checkInteractions();
|
||||
});
|
||||
player = video;
|
||||
}
|
||||
}
|
||||
|
||||
function startTimeTracking() {
|
||||
if (checkInterval) clearInterval(checkInterval);
|
||||
|
||||
checkInterval = setInterval(() => {
|
||||
if (playerType === 'youtube' && player && player.getCurrentTime) {
|
||||
currentTime = player.getCurrentTime();
|
||||
checkInteractions();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function checkInteractions() {
|
||||
data.interactions.forEach((interaction, index) => {
|
||||
const interactionTime = timeToSeconds(interaction.time);
|
||||
|
||||
// Check if we're at this interaction time (within 1 second window)
|
||||
if (Math.abs(currentTime - interactionTime) < 0.5 && !completedInteractions.has(index)) {
|
||||
showInteraction(interaction, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showInteraction(interaction, index) {
|
||||
// Pause video
|
||||
if (playerType === 'youtube' && player && player.pauseVideo) {
|
||||
player.pauseVideo();
|
||||
} else if (playerType === 'vimeo' && player && player.pause) {
|
||||
player.pause();
|
||||
} else if (playerType === 'html5' && player && player.pause) {
|
||||
player.pause();
|
||||
}
|
||||
|
||||
// Show overlay
|
||||
const overlay = document.getElementById('interactionOverlay');
|
||||
const icon = document.getElementById('interactionIcon');
|
||||
const title = document.getElementById('interactionTitle');
|
||||
const text = document.getElementById('interactionText');
|
||||
const extra = document.getElementById('interactionExtra');
|
||||
|
||||
// Set icon based on type
|
||||
switch (interaction.type) {
|
||||
case 'question':
|
||||
icon.textContent = '❓';
|
||||
break;
|
||||
case 'info':
|
||||
icon.textContent = 'ℹ️';
|
||||
break;
|
||||
case 'link':
|
||||
icon.textContent = '🔗';
|
||||
break;
|
||||
}
|
||||
|
||||
title.textContent = interaction.title;
|
||||
text.textContent = interaction.content;
|
||||
|
||||
// Add link if type is link
|
||||
if (interaction.type === 'link') {
|
||||
extra.innerHTML = `<a href="${interaction.content}" target="_blank" class="interaction-link">🔗 Link öffnen</a>`;
|
||||
} else {
|
||||
extra.innerHTML = '';
|
||||
}
|
||||
|
||||
overlay.classList.add('show');
|
||||
completedInteractions.add(index);
|
||||
|
||||
// Update timeline
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
function closeInteraction() {
|
||||
const overlay = document.getElementById('interactionOverlay');
|
||||
overlay.classList.remove('show');
|
||||
|
||||
// Resume video
|
||||
if (playerType === 'youtube' && player && player.playVideo) {
|
||||
player.playVideo();
|
||||
} else if (playerType === 'vimeo' && player && player.play) {
|
||||
player.play();
|
||||
} else if (playerType === 'html5' && player && player.play) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const container = document.getElementById('timelineMarkers');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.interactions.forEach((interaction, index) => {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'timeline-marker';
|
||||
if (completedInteractions.has(index)) {
|
||||
marker.classList.add('completed');
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
question: '❓',
|
||||
info: 'ℹ️',
|
||||
link: '🔗'
|
||||
};
|
||||
|
||||
marker.textContent = `${iconMap[interaction.type]} ${interaction.time} - ${interaction.title}`;
|
||||
marker.onclick = () => seekTo(interaction.time);
|
||||
|
||||
container.appendChild(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function seekTo(timeStr) {
|
||||
const seconds = timeToSeconds(timeStr);
|
||||
|
||||
if (playerType === 'youtube' && player && player.seekTo) {
|
||||
player.seekTo(seconds, true);
|
||||
} else if (playerType === 'vimeo' && player && player.setCurrentTime) {
|
||||
player.setCurrentTime(seconds);
|
||||
} else if (playerType === 'html5' && player) {
|
||||
player.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
function timeToSeconds(timeStr) {
|
||||
const parts = timeStr.split(':');
|
||||
const minutes = parseInt(parts[0]) || 0;
|
||||
const seconds = parseInt(parts[1]) || 0;
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
340
h5p-service/players/memory-player.html
Normal file
340
h5p-service/players/memory-player.html
Normal file
@@ -0,0 +1,340 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memory Game - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.stat {
|
||||
flex: 1;
|
||||
background: #f9fafb;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-label {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.stat-value {
|
||||
color: #1f2937;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
aspect-ratio: 1;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.card.matched {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
.card-face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.card-front {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
}
|
||||
.card-back {
|
||||
background: white;
|
||||
color: #1f2937;
|
||||
transform: rotateY(180deg);
|
||||
border: 3px solid #e5e7eb;
|
||||
}
|
||||
.card.matched .card-back {
|
||||
background: #d1fae5;
|
||||
border-color: #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
.results {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
display: none;
|
||||
}
|
||||
.results h2 {
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.results .trophy {
|
||||
font-size: 80px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.results .time {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 id="title"></h1>
|
||||
<p id="description" style="color: #6b7280; margin-top: 8px;"></p>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-label">Züge</div>
|
||||
<div class="stat-value" id="moves">0</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Gefunden</div>
|
||||
<div class="stat-value"><span id="matched">0</span> / <span id="total">0</span></div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Zeit</div>
|
||||
<div class="stat-value" id="time">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-board" id="gameBoard"></div>
|
||||
|
||||
<div class="results" id="results">
|
||||
<div class="trophy">🏆</div>
|
||||
<h2>Geschafft!</h2>
|
||||
<p style="color: #6b7280; margin: 12px 0;">Du hast alle Paare gefunden!</p>
|
||||
<div class="time" id="finalTime"></div>
|
||||
<p style="color: #6b7280;"><span id="finalMoves"></span> Züge</p>
|
||||
<button class="btn" onclick="restart()">🔄 Nochmal spielen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
let cards = [];
|
||||
let flippedCards = [];
|
||||
let matchedPairs = 0;
|
||||
let moves = 0;
|
||||
let startTime = null;
|
||||
let timerInterval = null;
|
||||
let canFlip = true;
|
||||
|
||||
function loadGame() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('description').textContent = data.description || '';
|
||||
document.getElementById('total').textContent = data.pairs.length;
|
||||
|
||||
createCards();
|
||||
renderBoard();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
function createCards() {
|
||||
// Create card array (2 cards per pair)
|
||||
cards = [];
|
||||
data.pairs.forEach((pair, index) => {
|
||||
cards.push({ id: `${index}-1`, pairId: index, text: pair.card1 });
|
||||
cards.push({ id: `${index}-2`, pairId: index, text: pair.card2 });
|
||||
});
|
||||
|
||||
// Shuffle cards
|
||||
cards = cards.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
const board = document.getElementById('gameBoard');
|
||||
board.innerHTML = '';
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
const cardEl = document.createElement('div');
|
||||
cardEl.className = 'card';
|
||||
cardEl.dataset.cardId = card.id;
|
||||
cardEl.dataset.pairId = card.pairId;
|
||||
cardEl.innerHTML = `
|
||||
<div class="card-face card-front">🧠</div>
|
||||
<div class="card-face card-back">${card.text}</div>
|
||||
`;
|
||||
cardEl.addEventListener('click', () => flipCard(cardEl, card));
|
||||
board.appendChild(cardEl);
|
||||
});
|
||||
}
|
||||
|
||||
function flipCard(cardEl, card) {
|
||||
if (!canFlip) return;
|
||||
if (cardEl.classList.contains('flipped')) return;
|
||||
if (cardEl.classList.contains('matched')) return;
|
||||
|
||||
cardEl.classList.add('flipped');
|
||||
flippedCards.push({ element: cardEl, card });
|
||||
|
||||
if (flippedCards.length === 2) {
|
||||
canFlip = false;
|
||||
moves++;
|
||||
document.getElementById('moves').textContent = moves;
|
||||
checkMatch();
|
||||
}
|
||||
}
|
||||
|
||||
function checkMatch() {
|
||||
const [first, second] = flippedCards;
|
||||
|
||||
if (first.card.pairId === second.card.pairId) {
|
||||
// Match!
|
||||
setTimeout(() => {
|
||||
first.element.classList.add('matched');
|
||||
second.element.classList.add('matched');
|
||||
flippedCards = [];
|
||||
canFlip = true;
|
||||
matchedPairs++;
|
||||
document.getElementById('matched').textContent = matchedPairs;
|
||||
|
||||
if (matchedPairs === data.pairs.length) {
|
||||
endGame();
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
// No match
|
||||
setTimeout(() => {
|
||||
first.element.classList.remove('flipped');
|
||||
second.element.classList.remove('flipped');
|
||||
flippedCards = [];
|
||||
canFlip = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
document.getElementById('time').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
clearInterval(timerInterval);
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
|
||||
document.getElementById('finalTime').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
document.getElementById('finalMoves').textContent = moves;
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('results').style.display = 'block';
|
||||
document.querySelector('.game-board').style.display = 'none';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function restart() {
|
||||
matchedPairs = 0;
|
||||
moves = 0;
|
||||
flippedCards = [];
|
||||
canFlip = true;
|
||||
|
||||
document.getElementById('moves').textContent = 0;
|
||||
document.getElementById('matched').textContent = 0;
|
||||
document.getElementById('results').style.display = 'none';
|
||||
document.querySelector('.game-board').style.display = 'grid';
|
||||
|
||||
createCards();
|
||||
renderBoard();
|
||||
clearInterval(timerInterval);
|
||||
startTimer();
|
||||
}
|
||||
|
||||
loadGame();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
380
h5p-service/players/quiz-player.html
Normal file
380
h5p-service/players/quiz-player.html
Normal file
@@ -0,0 +1,380 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quiz Player - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.description {
|
||||
color: #6b7280;
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.question-card {
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.question-number {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.question-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.answer {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.answer:hover {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
.answer.selected {
|
||||
border-color: #667eea;
|
||||
background: #f0f4ff;
|
||||
}
|
||||
.answer.correct {
|
||||
border-color: #10b981;
|
||||
background: #d1fae5;
|
||||
}
|
||||
.answer.incorrect {
|
||||
border-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
}
|
||||
.answer .checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #9ca3af;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.answer.selected .checkbox {
|
||||
background: #667eea;
|
||||
border-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.answer.correct .checkbox {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.answer.incorrect .checkbox {
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.answer-text {
|
||||
flex: 1;
|
||||
}
|
||||
.feedback {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
.feedback.correct {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border: 1px solid #10b981;
|
||||
display: block;
|
||||
}
|
||||
.feedback.incorrect {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #ef4444;
|
||||
display: block;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.results {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
display: none;
|
||||
}
|
||||
.results h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.results .score {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.results .percentage {
|
||||
font-size: 24px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.progress-bar {
|
||||
background: #e5e7eb;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
background: #667eea;
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
||||
</div>
|
||||
|
||||
<h1 id="quizTitle"></h1>
|
||||
<p class="description" id="quizDescription"></p>
|
||||
|
||||
<div class="results" id="results">
|
||||
<h2>🎉 Quiz abgeschlossen!</h2>
|
||||
<div class="score" id="scoreDisplay"></div>
|
||||
<div class="percentage" id="percentageDisplay"></div>
|
||||
</div>
|
||||
|
||||
<div id="questionsContainer"></div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="checkBtn" onclick="checkAnswers()" disabled>
|
||||
Antworten überprüfen
|
||||
</button>
|
||||
<button class="btn btn-primary" id="nextBtn" onclick="nextQuestion()" style="display: none;">
|
||||
Nächste Frage →
|
||||
</button>
|
||||
<button class="btn btn-primary" id="finishBtn" onclick="finishQuiz()" style="display: none;">
|
||||
Quiz beenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let quizData = null;
|
||||
let currentQuestion = 0;
|
||||
let score = 0;
|
||||
let answers = [];
|
||||
|
||||
function loadQuiz() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
quizData = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden des Quiz!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!quizData) {
|
||||
alert('Keine Quiz-Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('quizTitle').textContent = quizData.title;
|
||||
document.getElementById('quizDescription').textContent = quizData.description || '';
|
||||
|
||||
answers = new Array(quizData.questions.length).fill(null).map(() => []);
|
||||
|
||||
showQuestion(0);
|
||||
}
|
||||
|
||||
function showQuestion(index) {
|
||||
currentQuestion = index;
|
||||
const question = quizData.questions[index];
|
||||
const container = document.getElementById('questionsContainer');
|
||||
|
||||
// Update progress
|
||||
const progress = ((index + 1) / quizData.questions.length) * 100;
|
||||
document.getElementById('progressFill').style.width = progress + '%';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="question-card">
|
||||
<div class="question-number">Frage ${index + 1} von ${quizData.questions.length}</div>
|
||||
<div class="question-text">${question.text}</div>
|
||||
${question.answers.filter(a => a.text).map((answer, aIndex) => `
|
||||
<div class="answer" id="answer_${aIndex}" onclick="selectAnswer(${aIndex})">
|
||||
<div class="checkbox"></div>
|
||||
<div class="answer-text">${answer.text}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="feedback" id="feedback"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Reset buttons
|
||||
document.getElementById('checkBtn').style.display = 'inline-block';
|
||||
document.getElementById('checkBtn').disabled = true;
|
||||
document.getElementById('nextBtn').style.display = 'none';
|
||||
document.getElementById('finishBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
function selectAnswer(answerIndex) {
|
||||
const question = quizData.questions[currentQuestion];
|
||||
|
||||
// Check if multiple answers are allowed
|
||||
const correctCount = question.answers.filter(a => a.correct).length;
|
||||
const isMultipleChoice = correctCount > 1;
|
||||
|
||||
if (!isMultipleChoice) {
|
||||
// Single choice - deselect all others
|
||||
answers[currentQuestion] = [answerIndex];
|
||||
document.querySelectorAll('.answer').forEach((el, idx) => {
|
||||
el.classList.toggle('selected', idx === answerIndex);
|
||||
});
|
||||
} else {
|
||||
// Multiple choice - toggle
|
||||
const answerEl = document.getElementById('answer_' + answerIndex);
|
||||
const isSelected = answerEl.classList.contains('selected');
|
||||
|
||||
if (isSelected) {
|
||||
answerEl.classList.remove('selected');
|
||||
answers[currentQuestion] = answers[currentQuestion].filter(a => a !== answerIndex);
|
||||
} else {
|
||||
answerEl.classList.add('selected');
|
||||
answers[currentQuestion].push(answerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable check button if at least one answer is selected
|
||||
document.getElementById('checkBtn').disabled = answers[currentQuestion].length === 0;
|
||||
}
|
||||
|
||||
function checkAnswers() {
|
||||
const question = quizData.questions[currentQuestion];
|
||||
const userAnswers = answers[currentQuestion];
|
||||
const feedback = document.getElementById('feedback');
|
||||
|
||||
let isCorrect = true;
|
||||
|
||||
// Check each answer
|
||||
question.answers.forEach((answer, index) => {
|
||||
if (!answer.text) return; // Skip empty answers
|
||||
|
||||
const answerEl = document.getElementById('answer_' + index);
|
||||
const isSelected = userAnswers.includes(index);
|
||||
const shouldBeCorrect = answer.correct;
|
||||
|
||||
if (shouldBeCorrect) {
|
||||
answerEl.classList.add('correct');
|
||||
if (!isSelected) {
|
||||
isCorrect = false;
|
||||
}
|
||||
} else if (isSelected) {
|
||||
answerEl.classList.add('incorrect');
|
||||
isCorrect = false;
|
||||
}
|
||||
|
||||
// Disable clicking
|
||||
answerEl.style.cursor = 'default';
|
||||
answerEl.onclick = null;
|
||||
});
|
||||
|
||||
// Show feedback
|
||||
if (isCorrect) {
|
||||
feedback.className = 'feedback correct';
|
||||
feedback.textContent = '✅ Richtig! Sehr gut!';
|
||||
score++;
|
||||
} else {
|
||||
feedback.className = 'feedback incorrect';
|
||||
feedback.textContent = '❌ Leider falsch. Die richtigen Antworten sind grün markiert.';
|
||||
}
|
||||
|
||||
// Show appropriate button
|
||||
document.getElementById('checkBtn').style.display = 'none';
|
||||
|
||||
if (currentQuestion < quizData.questions.length - 1) {
|
||||
document.getElementById('nextBtn').style.display = 'inline-block';
|
||||
} else {
|
||||
document.getElementById('finishBtn').style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
function nextQuestion() {
|
||||
if (currentQuestion < quizData.questions.length - 1) {
|
||||
showQuestion(currentQuestion + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function finishQuiz() {
|
||||
const percentage = Math.round((score / quizData.questions.length) * 100);
|
||||
|
||||
document.getElementById('scoreDisplay').textContent = `${score} / ${quizData.questions.length}`;
|
||||
document.getElementById('percentageDisplay').textContent = `${percentage}% richtig`;
|
||||
|
||||
document.getElementById('results').style.display = 'block';
|
||||
document.getElementById('questionsContainer').style.display = 'none';
|
||||
document.querySelector('.btn-group').style.display = 'none';
|
||||
}
|
||||
|
||||
// Load quiz on page load
|
||||
loadQuiz();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
210
h5p-service/players/timeline-player.html
Normal file
210
h5p-service/players/timeline-player.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Timeline - BreakPilot H5P</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 32px;
|
||||
}
|
||||
.description {
|
||||
color: #6b7280;
|
||||
margin-bottom: 40px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.event {
|
||||
position: relative;
|
||||
margin-bottom: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.6s forwards;
|
||||
}
|
||||
.event:nth-child(odd) {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.event-content {
|
||||
width: calc(50% - 40px);
|
||||
background: #f9fafb;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.event-content:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
|
||||
border-color: #667eea;
|
||||
}
|
||||
.event:nth-child(odd) .event-content {
|
||||
margin-left: 40px;
|
||||
}
|
||||
.event:nth-child(even) .event-content {
|
||||
margin-right: 40px;
|
||||
}
|
||||
.event-marker {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
border: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
.event-year {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.event-title {
|
||||
color: #1f2937;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.event-description {
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
.event-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.timeline::before {
|
||||
left: 20px;
|
||||
}
|
||||
.event {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
.event-content {
|
||||
width: calc(100% - 60px);
|
||||
margin-left: 60px !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.event-marker {
|
||||
left: 20px;
|
||||
}
|
||||
.event-year {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title"></h1>
|
||||
<p class="description" id="description"></p>
|
||||
|
||||
<div class="timeline" id="timeline"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
|
||||
const icons = ['📍', '🎯', '⭐', '🏆', '💡', '🚀', '🎓', '🏛️', '🎨', '🌟'];
|
||||
|
||||
function loadContent() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dataParam = urlParams.get('data');
|
||||
|
||||
if (dataParam) {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(dataParam));
|
||||
} catch (e) {
|
||||
alert('Fehler beim Laden!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
alert('Keine Daten gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('title').textContent = data.title;
|
||||
document.getElementById('description').textContent = data.description || '';
|
||||
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const timeline = document.getElementById('timeline');
|
||||
timeline.innerHTML = '';
|
||||
|
||||
data.events.forEach((event, index) => {
|
||||
const eventEl = document.createElement('div');
|
||||
eventEl.className = 'event';
|
||||
eventEl.style.animationDelay = `${index * 0.1}s`;
|
||||
|
||||
const icon = icons[index % icons.length];
|
||||
|
||||
eventEl.innerHTML = `
|
||||
<div class="event-content">
|
||||
<span class="event-icon">${icon}</span>
|
||||
<div class="event-title">${event.title}</div>
|
||||
<div class="event-description">${event.description || ''}</div>
|
||||
</div>
|
||||
<div class="event-marker"></div>
|
||||
<div class="event-year">${event.year}</div>
|
||||
`;
|
||||
|
||||
timeline.appendChild(eventEl);
|
||||
});
|
||||
}
|
||||
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user