medienkompetenz-lernplattform/frontend/src/pages/LessonView.jsx
Marius Rometsch a439873394 Add lessons
2026-02-08 19:47:21 +01:00

302 lines
12 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import { participantAPI } from '../services/api.service';
import SQLShopDemo from '../components/lessons/InteractiveContent/SQLShopDemo';
import BitBDemo from '../components/lessons/InteractiveContent/BitBDemo';
import XSSDeeplinkDemo from '../components/lessons/InteractiveContent/XSSDeeplinkDemo';
import ForumScriptDemo from '../components/lessons/InteractiveContent/ForumScriptDemo';
import SocialMediaPasswordDemo from '../components/lessons/InteractiveContent/SocialMediaPasswordDemo';
import IDORDemo from '../components/lessons/InteractiveContent/IDORDemo';
const LessonView = () => {
const { eventLessonId } = useParams();
const navigate = useNavigate();
const [lesson, setLesson] = useState(null);
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [answers, setAnswers] = useState({});
const [feedback, setFeedback] = useState({});
const [totalScore, setTotalScore] = useState(0);
const [loading, setLoading] = useState(true);
const [completedInteractiveSteps, setCompletedInteractiveSteps] = useState(new Set());
useEffect(() => {
loadLesson();
}, [eventLessonId]);
const loadLesson = async () => {
try {
const response = await participantAPI.getLessonContent(eventLessonId);
const lessonData = response.data.data;
// Check if lesson is already completed
if (lessonData.progress?.status === 'completed') {
// Redirect back to event page - lesson already completed
navigate('/event');
return;
}
setLesson(lessonData);
await participantAPI.startLesson(eventLessonId);
} catch (error) {
console.error('Failed to load lesson:', error);
} finally {
setLoading(false);
}
};
const handleAnswer = async (questionId, answer) => {
try {
const response = await participantAPI.submitAnswer(eventLessonId, questionId, answer);
const result = response.data.data;
setFeedback(prev => ({ ...prev, [questionId]: result }));
setTotalScore(result.totalScore);
} catch (error) {
console.error('Failed to submit answer:', error);
}
};
const handleComplete = async () => {
try {
await participantAPI.completeLesson(eventLessonId);
navigate('/event');
} catch (error) {
console.error('Failed to complete lesson:', error);
}
};
if (loading || !lesson) {
return <div style={{ padding: '2rem', textAlign: 'center' }}>Loading lesson...</div>;
}
const currentStep = lesson.steps[currentStepIndex];
const isLastStep = currentStepIndex === lesson.steps.length - 1;
// Check if Previous button should be locked
// Lock if any interactive step between 0 and current index has been completed
const isPreviousLocked = Array.from(completedInteractiveSteps).some(
completedIndex => completedIndex < currentStepIndex
);
// Handler for Next button - mark interactive steps as completed
const handleNext = () => {
if (currentStep.type === 'interactive') {
setCompletedInteractiveSteps(prev => new Set([...prev, currentStepIndex]));
}
setCurrentStepIndex(currentStepIndex + 1);
};
return (
<div style={{ minHeight: '100vh', background: '#f9fafb' }}>
<nav style={{ background: 'white', padding: '1rem 2rem', borderBottom: '1px solid #e5e7eb' }}>
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
<h3 style={{ margin: 0, color: '#2563eb' }}>{lesson.title}</h3>
<div style={{ fontSize: '0.875rem', color: '#6b7280', marginTop: '0.5rem' }}>
Step {currentStepIndex + 1} of {lesson.steps.length} | Score: {totalScore}/{lesson.maxPoints}
</div>
</div>
</nav>
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '2rem' }}>
<div className="card">
<h2>{currentStep.title}</h2>
{currentStep.type === 'content' && (
<div style={{ lineHeight: '1.8' }} className="markdown-content">
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
</div>
)}
{currentStep.type === 'interactive' && (
<div>
{currentStep.content && (
<div style={{ lineHeight: '1.8', marginBottom: '1.5rem' }} className="markdown-content">
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
</div>
)}
{currentStep.interactiveComponent === 'SQLShopDemo' && (
<SQLShopDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'BitBDemo' && (
<BitBDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'XSSDeeplinkDemo' && (
<XSSDeeplinkDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'ForumScriptDemo' && (
<ForumScriptDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'SocialMediaPasswordDemo' && (
<SocialMediaPasswordDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'IDORDemo' && (
<IDORDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
</div>
)}
{currentStep.type === 'question' && (
<div>
<p style={{ fontWeight: '500', marginBottom: '1rem' }}>{currentStep.question}</p>
{currentStep.questionType === 'single_choice' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{currentStep.options.map(option => (
<label
key={option.id}
style={{
padding: '1rem',
border: '2px solid #e5e7eb',
borderRadius: '0.375rem',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
<input
type="radio"
name={currentStep.id}
value={option.id}
onChange={(e) => setAnswers(prev => ({ ...prev, [currentStep.id]: e.target.value }))}
style={{ marginRight: '0.5rem' }}
/>
{option.text}
</label>
))}
</div>
)}
{currentStep.questionType === 'multiple_choice' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{currentStep.options.map(option => (
<label
key={option.id}
style={{
padding: '1rem',
border: '2px solid #e5e7eb',
borderRadius: '0.375rem',
cursor: 'pointer'
}}
>
<input
type="checkbox"
value={option.id}
onChange={(e) => {
const current = answers[currentStep.id] || [];
const updated = e.target.checked
? [...current, option.id]
: current.filter(id => id !== option.id);
setAnswers(prev => ({ ...prev, [currentStep.id]: updated }));
}}
style={{ marginRight: '0.5rem' }}
/>
{option.text}
</label>
))}
</div>
)}
{currentStep.questionType === 'free_text' && (
<textarea
value={answers[currentStep.id] || ''}
onChange={(e) => setAnswers(prev => ({ ...prev, [currentStep.id]: e.target.value }))}
rows={5}
style={{ width: '100%', padding: '0.75rem', border: '1px solid #e5e7eb', borderRadius: '0.375rem' }}
placeholder="Type your answer here..."
/>
)}
{!feedback[currentStep.id] && (
<button
onClick={() => handleAnswer(currentStep.id, answers[currentStep.id])}
disabled={!answers[currentStep.id]}
style={{
marginTop: '1rem',
padding: '0.75rem 1.5rem',
background: '#2563eb',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer'
}}
>
Submit Answer
</button>
)}
{feedback[currentStep.id] && (
<div style={{
marginTop: '1rem',
padding: '1rem',
background: feedback[currentStep.id].isCorrect ? '#d1fae5' : '#fee2e2',
border: `1px solid ${feedback[currentStep.id].isCorrect ? '#10b981' : '#ef4444'}`,
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '500', marginBottom: '0.5rem' }}>
{feedback[currentStep.id].isCorrect ? '✅ Correct!' : '❌ Incorrect'}
</div>
<div>{feedback[currentStep.id].feedback}</div>
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem' }}>
Points awarded: {feedback[currentStep.id].pointsAwarded}
</div>
</div>
)}
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2rem' }}>
<button
onClick={() => setCurrentStepIndex(Math.max(0, currentStepIndex - 1))}
disabled={currentStepIndex === 0 || isPreviousLocked}
style={{
padding: '0.75rem 1.5rem',
background: (currentStepIndex === 0 || isPreviousLocked) ? '#d1d5db' : '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: (currentStepIndex === 0 || isPreviousLocked) ? 'not-allowed' : 'pointer',
opacity: (currentStepIndex === 0 || isPreviousLocked) ? 0.5 : 1
}}
title={isPreviousLocked ? 'Cannot go back after completing interactive steps' : ''}
>
Previous
</button>
{isLastStep ? (
<button
onClick={handleComplete}
style={{
padding: '0.75rem 1.5rem',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer'
}}
>
Complete Lesson
</button>
) : (
<button
onClick={handleNext}
style={{
padding: '0.75rem 1.5rem',
background: '#2563eb',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer'
}}
>
Next
</button>
)}
</div>
</div>
</div>
</div>
);
};
export default LessonView;