diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 5934e6f..628dcc1 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router-dom';
import { useApp } from '../store/AppContext';
import Card from '../components/ui/Card';
import Button from '../components/ui/Button';
@@ -11,33 +12,35 @@ const Dashboard = () => {
return (
-
Leren
-
Jouw onderwerp deze week:
+
Learning
+
Your topic this week:
-
Te doen
+
To Do
- De Rol van de Product Owner
-
+ The Role of the Product Owner
+
+
+
-
Testen
-
Weektest 10 vragen
+
Testing
+
Weekly test — 10 questions
-
Te doen
+
To Do
- Rond eerst je leersessie af
+ Complete your learning session first
@@ -45,29 +48,28 @@ const Dashboard = () => {
-
Recente Activiteit
+
Recent Activity
- {/* Placeholder activity items */}
T
-
Test afgerond: Informatiebeveiliging
-
Vorige week • Score: 90%
+
Test completed: Information Security
+
Last week · Score: 90%
-
+15 pt
+
+15 pts
L
-
Leersessie: Informatiebeveiliging
-
Vorige week
+
Learning session: Information Security
+
Last week
-
+15 pt
+
+5 pts
@@ -77,23 +79,24 @@ const Dashboard = () => {
Mini Leaderboard
- {/* Placeholder leaderboard items */}
1.
Admin
-
120 pt
+
120 pts
2.
{currentUser?.name}
-
85 pt
+
85 pts
-
+
+
+
diff --git a/src/pages/Leren.jsx b/src/pages/Leren.jsx
index 9cf5d1a..f18b42c 100644
--- a/src/pages/Leren.jsx
+++ b/src/pages/Leren.jsx
@@ -1,18 +1,19 @@
-import React, { useState, useEffect, useRef } from 'react';
-import { ChevronLeft, ChevronRight, BookOpen, Presentation, Mic, CheckCircle, Loader, ArrowRight, Volume2, VolumeX, BarChart2 } from 'lucide-react';
-import { motion, AnimatePresence } from 'framer-motion';
+import React, { useState, useEffect } from 'react';
+import { BookOpen, CheckCircle, Loader, ArrowRight } from 'lucide-react';
+import { motion } from 'framer-motion';
+import { Link } from 'react-router-dom';
import Card from '../components/ui/Card';
import Button from '../components/ui/Button';
import Tag from '../components/ui/Tag';
+import LearningContentViewer from '../components/ui/LearningContentViewer';
import { useApp } from '../store/AppContext';
-import { getAssignedTopic, generateLearningContent } from '../lib/learningService';
+import { getAssignedTopic, generateLearningContent, getCachedContent } from '../lib/learningService';
import { storage } from '../lib/storage';
const Leren = () => {
const { state } = useApp();
const [topic, setTopic] = useState(null);
const [content, setContent] = useState(null);
- const [activeMode, setActiveMode] = useState('artikel'); // 'artikel' | 'slides' | 'podcast'
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [completed, setCompleted] = useState(false);
@@ -21,6 +22,13 @@ const Leren = () => {
if (state.currentUser) {
const assigned = getAssignedTopic(state.currentUser.id, state.weekNumber);
setTopic(assigned);
+ if (assigned) {
+ const cached = getCachedContent(assigned.id);
+ if (cached) setContent(cached);
+ }
+ // Check if already completed this week
+ const done = storage.get(`user:${state.currentUser.id}:week:${state.weekNumber}:learn_done`, false);
+ if (done) setCompleted(true);
}
}, [state.currentUser, state.weekNumber]);
@@ -43,27 +51,31 @@ const Leren = () => {
storage.set(`user:${state.currentUser.id}:week:${state.weekNumber}:learn_done`, true);
};
+ // ── No topics available ───────────────────────────────────
if (!topic) {
return (
-
Leerstation
-
Er zijn nog geen kennisonderwerpen beschikbaar. Vraag een admin om bronmateriaal te uploaden.
+
Learning Station
+
No knowledge topics are available yet. Ask an admin to upload source material.
);
}
+ // ── Session completed ─────────────────────────────────────
if (completed) {
return (
-
Leersessie voltooid!
-
Je hebt "{topic.label}" succesvol doorgenomen. Ga nu de weektest maken!
-
+
Learning session complete!
+
+ You have successfully reviewed "{topic.label}". Now take the weekly test!
+
+
+
+
);
}
@@ -80,75 +92,43 @@ const Leren = () => {
{topic.description}
- {/* Mode Selector */}
- {content && (
-
- {[
- { key: 'artikel', icon: BookOpen, label: 'Artikel' },
- { key: 'slides', icon: Presentation, label: 'Slides' },
- { key: 'podcast', icon: Mic, label: 'Podcast' },
- { key: 'infographic', icon: BarChart2, label: 'Infographic' },
- ].map(({ key, icon: Icon, label }) => (
-
- ))}
-
- )}
-
- {/* Content Area */}
+ {/* Generate prompt */}
{!content && !isLoading && (
- Klik op de knop om je gepersonaliseerde leerinhoud te genereren met AI.
-
+ Click the button to generate your personalized AI learning content.
+
)}
+ {/* Loading */}
{isLoading && (
- AI genereert je gepersonaliseerde leerinhoud...
- Dit kan 10-30 seconden duren.
+ AI is generating your personalized learning content...
+ This may take 10–30 seconds.
)}
+ {/* Error */}
{error && (
- Fout bij genereren
+ Generation failed
{error}
)}
-
- {content && (
-
- {activeMode === 'artikel' && }
- {activeMode === 'slides' && }
- {activeMode === 'podcast' && }
- {activeMode === 'infographic' && }
-
- )}
-
+ {/* Content Viewer */}
+ {content &&
}
+ {/* Complete button */}
{content && !completed && (
)}
@@ -156,236 +136,4 @@ const Leren = () => {
);
};
-/* ── Article Renderer ─────────────────────────────────────── */
-const ArticleView = ({ content }) => (
-
-
- {content.title}
- {content.intro}
-
-
- {content.sections?.map((section, i) => (
-
- {section.heading}
- {section.body}
-
- ))}
-
- {content.keyTakeaways?.length > 0 && (
-
- Kernpunten
-
- {content.keyTakeaways.map((point, i) => (
- -
- →
- {point}
-
- ))}
-
-
- )}
-
-);
-
-/* ── Slides Renderer ──────────────────────────────────────── */
-const SlidesView = ({ slides }) => {
- const [current, setCurrent] = useState(0);
- const total = slides?.length || 0;
-
- return (
-
-
-
-
-
-
- {current + 1} / {total}
- Slide
-
-
{slides[current]?.title}
-
- {slides[current]?.bullets?.map((bullet, i) => (
- -
- ▸
- {bullet}
-
- ))}
-
-
- {slides[current]?.speakerNote && (
-
- 💬 {slides[current].speakerNote}
-
- )}
-
-
-
-
-
-
-
- {slides?.map((_, i) => (
-
-
-
-
- );
-};
-
-/* ── Podcast Renderer ─────────────────────────────────────── */
-const PodcastView = ({ script, topicLabel }) => {
- const [isPlaying, setIsPlaying] = useState(false);
- const utteranceRef = useRef(null);
-
- const togglePlayback = () => {
- if (!('speechSynthesis' in window)) {
- alert('Text-to-speech wordt niet ondersteund door je browser.');
- return;
- }
-
- if (isPlaying) {
- window.speechSynthesis.cancel();
- setIsPlaying(false);
- } else {
- const utterance = new SpeechSynthesisUtterance(script);
- utterance.lang = 'nl-NL';
- utterance.rate = 0.95;
- utterance.onend = () => setIsPlaying(false);
- utteranceRef.current = utterance;
- window.speechSynthesis.speak(utterance);
- setIsPlaying(true);
- }
- };
-
- // Cleanup on unmount
- useEffect(() => () => window.speechSynthesis?.cancel(), []);
-
- return (
-
-
-
-
-
-
-
Podcast Aflevering
-
{topicLabel}
-
-
-
-
- {isPlaying && (
-
-
- {[0.3, 0.7, 1, 0.6, 0.4, 0.8, 0.5].map((h, i) => (
-
- ))}
-
-
Wordt afgespeeld...
-
- )}
-
-
-
- );
-};
-
-/* ── Infographic Renderer ─────────────────────────────────── */
-const InfographicView = ({ data, topicLabel }) => {
- if (!data) {
- return (
-
- Geen infographic data beschikbaar voor dit onderwerp.
-
- );
- }
-
- return (
-
- {/* Hero Header */}
-
-
-
-
{topicLabel}
-
{data.headline}
-
{data.tagline}
-
-
-
- {/* Stats Row */}
- {data.stats?.length > 0 && (
-
- {data.stats.map((stat, i) => (
-
-
- {stat.icon}
- {stat.value}
- {stat.label}
-
-
- ))}
-
- )}
-
- {/* Process Steps */}
- {data.steps?.length > 0 && (
-
- Stappen & Processen
-
- {data.steps.map((step, i) => (
-
-
- {step.icon}
-
-
-
- 0{step.number}
-
{step.title}
-
-
{step.description}
-
-
- ))}
-
-
- )}
-
- {/* Quote */}
- {data.quote && (
-
- )}
-
- );
-};
-
export default Leren;
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx
index 3e433fc..2e72e03 100644
--- a/src/pages/Login.jsx
+++ b/src/pages/Login.jsx
@@ -15,7 +15,7 @@ const Login = () => {
const [error, setError] = useState('');
const userOptions = [
- { value: '', label: 'Selecteer een gebruiker...' },
+ { value: '', label: 'Select a user...' },
...state.users.map(u => ({ value: u.id, label: u.name }))
];
@@ -24,7 +24,7 @@ const Login = () => {
setError('');
if (!selectedUser || !pin) {
- setError('Vul alle velden in.');
+ setError('Please fill in all fields.');
return;
}
@@ -32,7 +32,7 @@ const Login = () => {
if (success) {
navigate('/');
} else {
- setError('Onjuiste PIN.');
+ setError('Incorrect PIN.');
}
};
@@ -42,13 +42,13 @@ const Login = () => {
Respellion
-
Leerplatform Login
+
Learning Platform
diff --git a/src/pages/Testen.jsx b/src/pages/Testen.jsx
new file mode 100644
index 0000000..da3f527
--- /dev/null
+++ b/src/pages/Testen.jsx
@@ -0,0 +1,406 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import {
+ CheckSquare, Loader, AlertCircle, Trophy, ArrowRight,
+ Clock, CheckCircle, XCircle, BarChart2
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Link } from 'react-router-dom';
+import Card from '../components/ui/Card';
+import Button from '../components/ui/Button';
+import Tag from '../components/ui/Tag';
+import { useApp } from '../store/AppContext';
+import { generateWeeklyQuiz, getCachedQuiz, saveTestResult, getTestResult } from '../lib/testService';
+
+const TIMER_SECONDS = 300; // 5 minutes
+
+// ─── Helper: format mm:ss ──────────────────────────────────
+function formatTime(sec) {
+ const m = Math.floor(sec / 60);
+ const s = sec % 60;
+ return `${m}:${s.toString().padStart(2, '0')}`;
+}
+
+const Testen = () => {
+ const { state } = useApp();
+ const { currentUser, weekNumber } = state;
+
+ const [phase, setPhase] = useState('intro'); // intro | loading | quiz | review | results
+ const [quiz, setQuiz] = useState(null);
+ const [answers, setAnswers] = useState({}); // { questionId: selectedIndex }
+ const [currentQ, setCurrentQ] = useState(0);
+ const [showFeedback, setShowFeedback] = useState(false);
+ const [timeLeft, setTimeLeft] = useState(TIMER_SECONDS);
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+ const timerRef = useRef(null);
+
+ // ── Check for existing result ──
+ useEffect(() => {
+ if (currentUser) {
+ const existing = getTestResult(currentUser.id, weekNumber);
+ if (existing) {
+ setResult(existing);
+ setPhase('results');
+ }
+ }
+ }, [currentUser, weekNumber]);
+
+ // ── Timer ──
+ useEffect(() => {
+ if (phase === 'quiz') {
+ timerRef.current = setInterval(() => {
+ setTimeLeft(prev => {
+ if (prev <= 1) {
+ clearInterval(timerRef.current);
+ finishQuiz();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ }
+ return () => clearInterval(timerRef.current);
+ }, [phase]);
+
+ // ── Start quiz ──
+ const startQuiz = async () => {
+ setPhase('loading');
+ setError(null);
+ try {
+ const q = await generateWeeklyQuiz(currentUser.id, weekNumber);
+ setQuiz(q);
+ setCurrentQ(0);
+ setAnswers({});
+ setShowFeedback(false);
+ setTimeLeft(TIMER_SECONDS);
+ setPhase('quiz');
+ } catch (e) {
+ setError(e.message);
+ setPhase('intro');
+ }
+ };
+
+ // ── Select answer ──
+ const selectAnswer = (questionId, optionIndex) => {
+ if (showFeedback) return; // locked
+ setAnswers(prev => ({ ...prev, [questionId]: optionIndex }));
+ setShowFeedback(true);
+ };
+
+ // ── Next question / finish ──
+ const nextQuestion = () => {
+ setShowFeedback(false);
+ if (currentQ < quiz.questions.length - 1) {
+ setCurrentQ(prev => prev + 1);
+ } else {
+ finishQuiz();
+ }
+ };
+
+ // ── Finish quiz & score ──
+ const finishQuiz = useCallback(() => {
+ clearInterval(timerRef.current);
+ if (!quiz) return;
+
+ const questions = quiz.questions;
+ let score = 0;
+ const breakdown = questions.map(q => {
+ const selected = answers[q.id];
+ const correct = selected === q.correctIndex;
+ if (correct) score++;
+ return {
+ questionId: q.id,
+ question: q.question,
+ topicLabel: q.topicLabel,
+ selected,
+ correctIndex: q.correctIndex,
+ correct,
+ explanation: q.explanation,
+ options: q.options,
+ };
+ });
+
+ const testResult = {
+ score,
+ total: questions.length,
+ percentage: Math.round((score / questions.length) * 100),
+ timeUsed: TIMER_SECONDS - timeLeft,
+ completedAt: new Date().toISOString(),
+ breakdown,
+ };
+
+ const { pointsEarned } = saveTestResult(currentUser.id, weekNumber, testResult);
+ testResult.pointsEarned = pointsEarned;
+ setResult(testResult);
+ setPhase('results');
+ }, [quiz, answers, timeLeft, currentUser, weekNumber]);
+
+ // ─── Intro / Start screen ────────────────────────────────
+ if (phase === 'intro') {
+ return (
+
+
+
+
Weekly Test
+
Week {weekNumber}
+
+ Test your knowledge with 10 AI-generated questions. You have 5 minutes to complete the test. Good luck!
+
+
+ {error && (
+
+
+
+ )}
+
+
+
+ 10 questions
+ 5 minutes
+ 20 pts max
+
+
+
+
+
+ );
+ }
+
+ // ─── Loading ──────────────────────────────────────────────
+ if (phase === 'loading') {
+ return (
+
+
+
AI is generating your test...
+
Preparing 10 questions based on your learning topics.
+
+ );
+ }
+
+ // ─── Results ──────────────────────────────────────────────
+ if (phase === 'results' && result) {
+ const isPerfect = result.percentage === 100;
+ const isGood = result.percentage >= 70;
+
+ return (
+
+ {/* Score Header */}
+
+
+ {result.percentage}%
+
+
+ {isPerfect ? 'Perfect score!' : isGood ? 'Well done!' : 'Keep learning!'}
+
+
+ You scored {result.score}/{result.total} in {formatTime(result.timeUsed)}.
+ {result.pointsEarned && +{result.pointsEarned} pts}
+
+
+
+ {/* Summary Cards */}
+
+
+
+ {result.score}
+ Correct
+
+
+
+ {result.total - result.score}
+ Incorrect
+
+
+
+ {formatTime(result.timeUsed)}
+ Time Used
+
+
+
+ {/* Review toggle */}
+
+
Question Review
+
+
+
+
+
+ {/* Question breakdown */}
+
+ {result.breakdown.map((item, i) => (
+
+
+
+ {i + 1}
+
+
+
{item.question}
+
{item.topicLabel}
+
+
+
+
+ {item.options.map((opt, oi) => {
+ const isCorrect = oi === item.correctIndex;
+ const wasSelected = oi === item.selected;
+ return (
+
+ {isCorrect && }
+ {wasSelected && !isCorrect && }
+ {opt}
+
+ );
+ })}
+
+
+ {item.explanation}
+
+ ))}
+
+
+ );
+ }
+
+ // ─── Active Quiz ──────────────────────────────────────────
+ if (phase === 'quiz' && quiz) {
+ const q = quiz.questions[currentQ];
+ const selectedAnswer = answers[q.id];
+ const isCorrect = selectedAnswer === q.correctIndex;
+ const progress = ((currentQ + (showFeedback ? 1 : 0)) / quiz.questions.length) * 100;
+ const timerDanger = timeLeft < 60;
+
+ return (
+
+ {/* Header bar */}
+
+
+ Question {currentQ + 1} of {quiz.questions.length}
+
+
+ {formatTime(timeLeft)}
+
+
+
+ {/* Progress bar */}
+
+
+
+
+ {/* Question card */}
+
+
+
+
+ {q.topicLabel}
+
+ {q.question}
+
+
+ {/* Options */}
+
+ {q.options.map((option, oi) => {
+ let optionClass = 'border-bg-warm bg-paper hover:border-teal/50 hover:bg-teal/5 cursor-pointer';
+
+ if (showFeedback) {
+ if (oi === q.correctIndex) {
+ optionClass = 'border-teal bg-teal/10 text-teal';
+ } else if (oi === selectedAnswer && !isCorrect) {
+ optionClass = 'border-red-300 bg-red-50 text-red-700';
+ } else {
+ optionClass = 'border-bg-warm text-fg-muted opacity-50';
+ }
+ } else if (selectedAnswer === oi) {
+ optionClass = 'border-teal bg-teal/5';
+ }
+
+ return (
+
+ );
+ })}
+
+
+ {/* Feedback */}
+ {showFeedback && (
+
+
+
+ {isCorrect
+ ?
+ :
}
+
+
+ {isCorrect ? 'Correct!' : 'Incorrect'}
+
+
{q.explanation}
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+ }
+
+ return null;
+};
+
+export default Testen;