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:
@@ -1,38 +1,13 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
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 (
|
||||
<Card className="w-full h-[60vh] flex flex-col p-0">
|
||||
<div className="p-6 border-b border-bg-warm">
|
||||
<h2 className="text-xl font-bold">Concept Explainer</h2>
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-6 prose dark:prose-invert"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto p-6 prose dark:prose-invert">
|
||||
{content?.sections?.map((section, i) => (
|
||||
<div key={i} className="mb-6">
|
||||
<h3>{section.title}</h3>
|
||||
@@ -40,6 +15,9 @@ export default function ConceptExplainer({ content, onComplete }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4 border-t border-bg-warm flex justify-end">
|
||||
<Button onClick={onComplete}>Finish session</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
@@ -11,14 +11,9 @@ export default function FlashcardSet({ content, onComplete }) {
|
||||
|
||||
const handleFlip = () => {
|
||||
setIsFlipped(!isFlipped);
|
||||
|
||||
const newViewed = new Set(viewedCards);
|
||||
newViewed.add(currentIndex);
|
||||
setViewedCards(newViewed);
|
||||
|
||||
if (newViewed.size === cards.length && cards.length > 0) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
@@ -68,6 +63,13 @@ export default function FlashcardSet({ content, onComplete }) {
|
||||
<Button variant="outline" onClick={handlePrev}>Previous</Button>
|
||||
<Button variant="outline" onClick={handleNext}>Next</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRef } from 'react';
|
||||
import ConceptExplainer from './ConceptExplainer';
|
||||
import ScenarioQuiz from './ScenarioQuiz';
|
||||
import FlashcardSet from './FlashcardSet';
|
||||
@@ -7,31 +6,33 @@ import { useMicroLearningCompletions } from '../../hooks/useMicroLearningComplet
|
||||
|
||||
export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) {
|
||||
const { recordCompletion } = useMicroLearningCompletions();
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
// Ref-guard against double-fire while the await is in flight — using state
|
||||
// would let a second click slip through before the re-render lands.
|
||||
const recordingRef = useRef(false);
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (completed) return; // Prevent double recording
|
||||
if (recordingRef.current) return;
|
||||
recordingRef.current = true;
|
||||
|
||||
const record = await recordCompletion({
|
||||
microLearningId: microLearning.id,
|
||||
topicId: microLearning.topic_id,
|
||||
type: microLearning.type,
|
||||
sessionWeek: sessionWeek
|
||||
sessionWeek: sessionWeek,
|
||||
});
|
||||
|
||||
if (record) {
|
||||
setCompleted(true);
|
||||
if (onCompletedSuccessfully) {
|
||||
onCompletedSuccessfully(record);
|
||||
}
|
||||
if (record && onCompletedSuccessfully) {
|
||||
onCompletedSuccessfully(record);
|
||||
} else if (!record) {
|
||||
// Allow a retry if persistence failed.
|
||||
recordingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderComponent = () => {
|
||||
const props = {
|
||||
content: microLearning.content,
|
||||
onComplete: handleComplete
|
||||
onComplete: handleComplete,
|
||||
};
|
||||
|
||||
switch (microLearning.type) {
|
||||
@@ -46,24 +47,5 @@ export default function MicroLearningContainer({ microLearning, sessionWeek, onC
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <div className="w-full max-w-3xl mx-auto">{renderComponent()}</div>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
@@ -10,7 +10,6 @@ export default function ScenarioQuiz({ content, onComplete }) {
|
||||
if (showExplanations) return;
|
||||
setSelectedOptionId(idx);
|
||||
setShowExplanations(true);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,6 +56,12 @@ export default function ScenarioQuiz({ content, onComplete }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{showExplanations && (
|
||||
<div className="pt-4 border-t border-bg-warm flex justify-end">
|
||||
<Button onClick={onComplete}>Finish session</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user