feat: Implement lesson attempt tracking, retry functionality, and max attempt limits for interactive blocks.

This commit is contained in:
2025-12-22 15:47:18 -03:00
parent 32f71852d9
commit f592f78b6c
16 changed files with 325 additions and 178 deletions
@@ -6,9 +6,10 @@ interface FillInTheBlanksPlayerProps {
id: string;
title?: string;
content: string;
allowRetry?: boolean;
}
export default function FillInTheBlanksPlayer({ id, title, content }: FillInTheBlanksPlayerProps) {
export default function FillInTheBlanksPlayer({ id, title, content, allowRetry = true }: FillInTheBlanksPlayerProps) {
const [userAnswers, setUserAnswers] = useState<string[]>([]);
const [submitted, setSubmitted] = useState(false);
@@ -66,8 +67,8 @@ export default function FillInTheBlanksPlayer({ id, title, content }: FillInTheB
}}
disabled={submitted}
className={`mx-1 px-2 py-0 border-b-2 bg-transparent transition-all focus:outline-none text-center rounded-t-sm ${submitted
? (isCorrect(part.index!) ? "border-green-500 text-green-400 bg-green-500/10" : "border-red-500 text-red-100 bg-red-500/10")
: "border-blue-500/30 focus:border-blue-500 text-blue-400 focus:bg-blue-500/5"
? (isCorrect(part.index!) ? "border-green-500 text-green-400 bg-green-500/10" : "border-red-500 text-red-100 bg-red-500/10")
: "border-blue-500/30 focus:border-blue-500 text-blue-400 focus:bg-blue-500/5"
}`}
style={{ width: `${Math.max((part.answer?.length || 5) * 12, 60)}px` }}
placeholder="..."
@@ -76,22 +77,26 @@ export default function FillInTheBlanksPlayer({ id, title, content }: FillInTheB
))}
</div>
{!submitted && parsed.answers.length > 0 && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Answers
</button>
)}
{allowRetry && (
<>
{!submitted && parsed.answers.length > 0 && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Answers
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-3xl border-white/5"
>
Try Again
</button>
)}
</>
)}
</div>
</div>
@@ -11,9 +11,10 @@ interface MatchingPlayerProps {
id: string;
title?: string;
pairs: MatchingPair[];
allowRetry?: boolean;
}
export default function MatchingPlayer({ id, title, pairs }: MatchingPlayerProps) {
export default function MatchingPlayer({ id, title, pairs, allowRetry = true }: MatchingPlayerProps) {
const [selectedLeft, setSelectedLeft] = useState<number | null>(null);
const [matches, setMatches] = useState<Record<number, number>>({});
const [submitted, setSubmitted] = useState(false);
@@ -52,8 +53,8 @@ export default function MatchingPlayer({ id, title, pairs }: MatchingPlayerProps
key={i}
onClick={() => !submitted && setSelectedLeft(i)}
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft === i ? "border-blue-500 bg-blue-500/10 text-white shadow-lg" :
matches[i] !== undefined ? "border-blue-500/20 bg-blue-500/5 text-blue-400" :
"border-white/5 bg-white/5 text-gray-200 hover:border-white/20"
matches[i] !== undefined ? "border-blue-500/20 bg-blue-500/5 text-blue-400" :
"border-white/5 bg-white/5 text-gray-200 hover:border-white/20"
}`}
>
{pair.left}
@@ -90,24 +91,26 @@ export default function MatchingPlayer({ id, title, pairs }: MatchingPlayerProps
})}
</div>
<div className="md:col-span-2 pt-8 border-t border-white/5">
{!submitted && Object.keys(matches).length === (pairs || []).length && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Matching
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
)}
</div>
{allowRetry && (
<div className="md:col-span-2 pt-8 border-t border-white/5">
{!submitted && Object.keys(matches).length === (pairs || []).length && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Matching
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
)}
</div>
)}
</div>
</div>
);
@@ -6,9 +6,10 @@ interface OrderingPlayerProps {
id: string;
title?: string;
items: string[];
allowRetry?: boolean;
}
export default function OrderingPlayer({ id, title, items }: OrderingPlayerProps) {
export default function OrderingPlayer({ id, title, items, allowRetry = true }: OrderingPlayerProps) {
const [userOrder, setUserOrder] = useState<number[]>([]);
const [submitted, setSubmitted] = useState(false);
@@ -53,7 +54,7 @@ export default function OrderingPlayer({ id, title, items }: OrderingPlayerProps
disabled={isPicked || submitted}
onClick={() => handlePick(item.originalIdx)}
className={`px-6 py-3 rounded-full border text-sm font-bold transition-all ${isPicked ? "opacity-20 grayscale border-white/5 bg-white/5" :
"border-white/10 bg-white/5 text-gray-200 hover:border-blue-500/50 hover:bg-blue-500/5"
"border-white/10 bg-white/5 text-gray-200 hover:border-blue-500/50 hover:bg-blue-500/5"
}`}
>
{item.value}
@@ -76,8 +77,8 @@ export default function OrderingPlayer({ id, title, items }: OrderingPlayerProps
key={i}
onClick={() => !submitted && handlePick(idx)}
className={`flex items-center gap-4 p-4 rounded-xl border text-sm font-bold transition-all cursor-pointer ${isItemCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
isItemWrong ? "border-red-500 bg-red-500/20 text-red-100" :
"border-blue-500/30 bg-blue-500/5 text-blue-400 hover:bg-blue-500/10"
isItemWrong ? "border-red-500 bg-red-500/20 text-red-100" :
"border-blue-500/30 bg-blue-500/5 text-blue-400 hover:bg-blue-500/10"
}`}
>
<span className="opacity-50 text-xs">{i + 1}.</span>
@@ -92,24 +93,26 @@ export default function OrderingPlayer({ id, title, items }: OrderingPlayerProps
</div>
</div>
<div className="pt-8 border-t border-white/5">
{!submitted && userOrder.length === (items || []).length && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Sequence
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
)}
</div>
{allowRetry && (
<div className="pt-8 border-t border-white/5">
{!submitted && userOrder.length === (items || []).length && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Sequence
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
)}
</div>
)}
</div>
</div>
);
@@ -16,9 +16,10 @@ interface QuizPlayerProps {
quizData: {
questions: QuizQuestion[];
};
allowRetry?: boolean;
}
export default function QuizPlayer({ id, title, quizData }: QuizPlayerProps) {
export default function QuizPlayer({ id, title, quizData, allowRetry = true }: QuizPlayerProps) {
const [userAnswers, setUserAnswers] = useState<Record<string, number[]>>({});
const [submitted, setSubmitted] = useState(false);
@@ -88,21 +89,25 @@ export default function QuizPlayer({ id, title, quizData }: QuizPlayerProps) {
</div>
))}
{!submitted && questions.length > 0 && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Answers
</button>
)}
{submitted && (
<button
onClick={() => { setSubmitted(false); setUserAnswers({}); }}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-3xl border-white/5"
>
Try Again
</button>
{allowRetry && (
<>
{!submitted && questions.length > 0 && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
>
Validate Answers
</button>
)}
{submitted && (
<button
onClick={() => { setSubmitted(false); setUserAnswers({}); }}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-3xl border-white/5"
>
Try Again
</button>
)}
</>
)}
</div>
</div>
@@ -7,9 +7,10 @@ interface ShortAnswerPlayerProps {
title?: string;
prompt: string;
correctAnswers: string[];
allowRetry?: boolean;
}
export default function ShortAnswerPlayer({ id, title, prompt, correctAnswers }: ShortAnswerPlayerProps) {
export default function ShortAnswerPlayer({ id, title, prompt, correctAnswers, allowRetry = true }: ShortAnswerPlayerProps) {
const [userAnswer, setUserAnswer] = useState("");
const [submitted, setSubmitted] = useState(false);
@@ -38,8 +39,8 @@ export default function ShortAnswerPlayer({ id, title, prompt, correctAnswers }:
onChange={(e) => setUserAnswer(e.target.value)}
disabled={submitted}
className={`w-full bg-white/5 border-2 rounded-2xl px-6 py-4 text-lg transition-all focus:outline-none ${submitted
? (isCorrect ? "border-green-500 bg-green-500/10 text-green-400" : "border-red-500 bg-red-500/10 text-red-100")
: "border-white/10 focus:border-blue-500 text-white"
? (isCorrect ? "border-green-500 bg-green-500/10 text-green-400" : "border-red-500 bg-red-500/10 text-red-100")
: "border-white/10 focus:border-blue-500 text-white"
}`}
placeholder="Type your answer..."
/>
@@ -52,23 +53,27 @@ export default function ShortAnswerPlayer({ id, title, prompt, correctAnswers }:
)}
</div>
{!submitted && (
<button
onClick={() => setSubmitted(true)}
disabled={!userAnswer.trim()}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 disabled:opacity-50 disabled:grayscale"
>
Submit Answer
</button>
)}
{allowRetry && (
<>
{!submitted && (
<button
onClick={() => setSubmitted(true)}
disabled={!userAnswer.trim()}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 disabled:opacity-50 disabled:grayscale"
>
Submit Answer
</button>
)}
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
>
Try Again
</button>
{submitted && (
<button
onClick={handleReset}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-3xl border-white/5"
>
Try Again
</button>
)}
</>
)}
</div>
</div>