fix: end micro-learning sessions explicitly and route to test

Every micro-learning format used to call onComplete implicitly — scroll
to the bottom of the explainer, flip the last flashcard, or pick any
scenario option — which yanked users out of the content before they
were done with it.

Replace the implicit triggers with a Finish session button on each
format. The scenario quiz exposes the button only after an option is
chosen so explanations still get read; the flashcard set exposes a
viewed-cards counter next to it.

The topic-reviewed landing screen now offers Back to Station, Try
another format, and Go to test, so finishing a session leads somewhere
useful instead of dead-ending. Drop the unreachable completion box that
the container was rendering underneath the parent's success screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RaymondVerhoef
2026-05-26 14:20:41 +02:00
parent 84e7468841
commit 9b3d7fde8c
5 changed files with 49 additions and 72 deletions

View File

@@ -1,38 +1,13 @@
import React, { useEffect, useRef } from 'react';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Button from '../ui/Button';
export default function ConceptExplainer({ content, onComplete }) { export default function ConceptExplainer({ content, onComplete }) {
const containerRef = useRef(null);
// Trigger completion when scrolled to the end
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
if (scrollTop + clientHeight >= scrollHeight - 50) {
onComplete();
}
};
// Check initially in case content is short and doesn't need scrolling
if (containerRef.current && containerRef.current.scrollHeight <= containerRef.current.clientHeight) {
onComplete();
}
const currentRef = containerRef.current;
currentRef?.addEventListener('scroll', handleScroll);
return () => currentRef?.removeEventListener('scroll', handleScroll);
}, [onComplete]);
return ( return (
<Card className="w-full h-[60vh] flex flex-col p-0"> <Card className="w-full h-[60vh] flex flex-col p-0">
<div className="p-6 border-b border-bg-warm"> <div className="p-6 border-b border-bg-warm">
<h2 className="text-xl font-bold">Concept Explainer</h2> <h2 className="text-xl font-bold">Concept Explainer</h2>
</div> </div>
<div <div className="flex-1 overflow-y-auto p-6 prose dark:prose-invert">
className="flex-1 overflow-y-auto p-6 prose dark:prose-invert"
ref={containerRef}
>
{content?.sections?.map((section, i) => ( {content?.sections?.map((section, i) => (
<div key={i} className="mb-6"> <div key={i} className="mb-6">
<h3>{section.title}</h3> <h3>{section.title}</h3>
@@ -40,6 +15,9 @@ export default function ConceptExplainer({ content, onComplete }) {
</div> </div>
))} ))}
</div> </div>
<div className="p-4 border-t border-bg-warm flex justify-end">
<Button onClick={onComplete}>Finish session</Button>
</div>
</Card> </Card>
); );
} }

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Button from '../ui/Button'; import Button from '../ui/Button';
@@ -11,14 +11,9 @@ export default function FlashcardSet({ content, onComplete }) {
const handleFlip = () => { const handleFlip = () => {
setIsFlipped(!isFlipped); setIsFlipped(!isFlipped);
const newViewed = new Set(viewedCards); const newViewed = new Set(viewedCards);
newViewed.add(currentIndex); newViewed.add(currentIndex);
setViewedCards(newViewed); setViewedCards(newViewed);
if (newViewed.size === cards.length && cards.length > 0) {
onComplete();
}
}; };
const handleNext = () => { const handleNext = () => {
@@ -68,6 +63,13 @@ export default function FlashcardSet({ content, onComplete }) {
<Button variant="outline" onClick={handlePrev}>Previous</Button> <Button variant="outline" onClick={handlePrev}>Previous</Button>
<Button variant="outline" onClick={handleNext}>Next</Button> <Button variant="outline" onClick={handleNext}>Next</Button>
</div> </div>
<div className="w-full max-w-lg pt-4 border-t border-bg-warm flex items-center justify-between">
<span className="text-xs text-fg-muted">
{viewedCards.size}/{cards.length} cards viewed
</span>
<Button onClick={onComplete}>Finish session</Button>
</div>
</div> </div>
); );
} }

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'; import { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import ConceptExplainer from './ConceptExplainer'; import ConceptExplainer from './ConceptExplainer';
import ScenarioQuiz from './ScenarioQuiz'; import ScenarioQuiz from './ScenarioQuiz';
import FlashcardSet from './FlashcardSet'; import FlashcardSet from './FlashcardSet';
@@ -7,31 +6,33 @@ import { useMicroLearningCompletions } from '../../hooks/useMicroLearningComplet
export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) { export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) {
const { recordCompletion } = useMicroLearningCompletions(); const { recordCompletion } = useMicroLearningCompletions();
const [completed, setCompleted] = useState(false); // Ref-guard against double-fire while the await is in flight — using state
const navigate = useNavigate(); // would let a second click slip through before the re-render lands.
const recordingRef = useRef(false);
const handleComplete = async () => { const handleComplete = async () => {
if (completed) return; // Prevent double recording if (recordingRef.current) return;
recordingRef.current = true;
const record = await recordCompletion({ const record = await recordCompletion({
microLearningId: microLearning.id, microLearningId: microLearning.id,
topicId: microLearning.topic_id, topicId: microLearning.topic_id,
type: microLearning.type, type: microLearning.type,
sessionWeek: sessionWeek sessionWeek: sessionWeek,
}); });
if (record) { if (record && onCompletedSuccessfully) {
setCompleted(true);
if (onCompletedSuccessfully) {
onCompletedSuccessfully(record); onCompletedSuccessfully(record);
} } else if (!record) {
// Allow a retry if persistence failed.
recordingRef.current = false;
} }
}; };
const renderComponent = () => { const renderComponent = () => {
const props = { const props = {
content: microLearning.content, content: microLearning.content,
onComplete: handleComplete onComplete: handleComplete,
}; };
switch (microLearning.type) { switch (microLearning.type) {
@@ -46,24 +47,5 @@ export default function MicroLearningContainer({ microLearning, sessionWeek, onC
} }
}; };
return ( return <div className="w-full max-w-3xl mx-auto">{renderComponent()}</div>;
<div className="w-full max-w-3xl mx-auto">
{renderComponent()}
{completed && (
<div className="mt-4 p-4 bg-green-50 text-green-800 border border-green-200 rounded-md text-center flex flex-col items-center">
<div>
<p className="font-semibold">Micro Learning Completed!</p>
<p className="text-sm mt-1 mb-4">Your progress has been recorded.</p>
</div>
<button
onClick={() => navigate('/test')}
className="px-4 py-2 bg-teal text-white rounded-md font-medium hover:bg-teal-dark transition-colors"
>
Start Test for this Week
</button>
</div>
)}
</div>
);
} }

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Button from '../ui/Button'; import Button from '../ui/Button';
@@ -10,7 +10,6 @@ export default function ScenarioQuiz({ content, onComplete }) {
if (showExplanations) return; if (showExplanations) return;
setSelectedOptionId(idx); setSelectedOptionId(idx);
setShowExplanations(true); setShowExplanations(true);
onComplete();
}; };
return ( return (
@@ -57,6 +56,12 @@ export default function ScenarioQuiz({ content, onComplete }) {
); );
})} })}
</div> </div>
{showExplanations && (
<div className="pt-4 border-t border-bg-warm flex justify-end">
<Button onClick={onComplete}>Finish session</Button>
</div>
)}
</div> </div>
</Card> </Card>
); );

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { BookOpen, CheckCircle, ArrowRight, ChevronLeft, Calendar } from 'lucide-react'; import { BookOpen, CheckCircle, ArrowRight, ChevronLeft, Calendar, CheckSquare } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Card from '../components/ui/Card'; import Card from '../components/ui/Card';
import Button from '../components/ui/Button'; import Button from '../components/ui/Button';
import Tag from '../components/ui/Tag'; import Tag from '../components/ui/Tag';
@@ -14,6 +14,7 @@ import { useMicroLearningCompletions } from '../hooks/useMicroLearningCompletion
const Leren = () => { const Leren = () => {
const { state } = useApp(); const { state } = useApp();
const navigate = useNavigate();
const [assignedTopic, setAssignedTopic] = useState(null); const [assignedTopic, setAssignedTopic] = useState(null);
const [allTopics, setAllTopics] = useState([]); const [allTopics, setAllTopics] = useState([]);
@@ -96,12 +97,21 @@ const Leren = () => {
<CheckCircle size={80} className="mx-auto text-teal mb-6" /> <CheckCircle size={80} className="mx-auto text-teal mb-6" />
</motion.div> </motion.div>
<h1 className="text-3xl font-bold mb-4">Topic reviewed!</h1> <h1 className="text-3xl font-bold mb-4">Topic reviewed!</h1>
<p className="text-fg-muted mb-8"> <p className="text-fg-muted mb-2">
You have successfully completed a micro learning for "<strong>{activeTopic.label}</strong>". You have successfully completed a micro learning for "<strong>{activeTopic.label}</strong>".
</p> </p>
<div className="flex justify-center gap-4"> <p className="text-fg-muted mb-8">
Ready to put it to the test, or explore another format for this topic?
</p>
<div className="flex flex-wrap justify-center gap-3">
<Button variant="outline" onClick={() => { setView('overview'); setSessionDone(false); }}> <Button variant="outline" onClick={() => { setView('overview'); setSessionDone(false); }}>
Back to Station <ChevronLeft size={16} className="mr-2" /> Back to Station
</Button>
<Button variant="outline" onClick={() => setSessionDone(false)}>
Try another format
</Button>
<Button onClick={() => navigate('/test')}>
<CheckSquare size={16} className="mr-2" /> Go to test
</Button> </Button>
</div> </div>
</div> </div>