feat: Introduce new interactive content blocks including Fill-in-the-Blanks, Short Answer, Ordering, and Matching, with corresponding API, database, and UI integration.

This commit is contained in:
2025-12-19 17:03:26 -03:00
parent 0988213eb7
commit 57b8d7c0a1
17 changed files with 1513 additions and 32 deletions
@@ -0,0 +1,132 @@
"use client";
import { useState, useMemo } from "react";
interface FillInTheBlanksBlockProps {
id: string;
title?: string;
content: string;
editMode: boolean;
onChange: (updates: { title?: string; content?: string }) => void;
}
export default function FillInTheBlanksBlock({ id, title, content, editMode, onChange }: FillInTheBlanksBlockProps) {
const [userAnswers, setUserAnswers] = useState<string[]>([]);
const [submitted, setSubmitted] = useState(false);
// Parse content to find blanks
const parsed = useMemo(() => {
const parts: { type: 'text' | 'blank'; value?: string; index?: number; answer?: string }[] = [];
const answers: string[] = [];
let lastIndex = 0;
const regex = /\[\[(.*?)\]\]/g;
let match;
while ((match = regex.exec(content)) !== null) {
// Push text before the blank
parts.push({ type: 'text', value: content.substring(lastIndex, match.index) });
// Push the blank
const answer = match[1];
parts.push({ type: 'blank', index: answers.length, answer });
answers.push(answer);
lastIndex = regex.lastIndex;
}
// Push remaining text
parts.push({ type: 'text', value: content.substring(lastIndex) });
return { parts, answers };
}, [content]);
const handleReset = () => {
setSubmitted(false);
setUserAnswers([]);
};
const isCorrect = (index: number) => {
return userAnswers[index]?.trim().toLowerCase() === parsed.answers[index]?.trim().toLowerCase();
};
return (
<div className="space-y-8" id={id}>
<div className="space-y-2">
{editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Section Title (Optional)</label>
<input
type="text"
value={title || ""}
onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Fill the gaps, Quote..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none"
/>
</div>
) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
{title || "Fill in the Blanks"}
</h3>
)}
</div>
{editMode ? (
<div className="space-y-4">
<div className="p-6 glass border-white/5 space-y-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Text with blanks (use [[answer]])</label>
<textarea
value={content}
onChange={(e) => onChange({ content: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[150px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all"
placeholder="Example: The [[capital]] of France is [[Paris]]."
/>
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Tip: Surround any word with double brackets to create a blank.</p>
</div>
</div>
) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
<div className="text-lg leading-loose text-gray-100">
{parsed.parts.map((part, i) => (
part.type === 'text' ? (
<span key={i}>{part.value}</span>
) : (
<input
key={i}
type="text"
value={userAnswers[part.index!] || ""}
onChange={(e) => {
const newAnswers = [...userAnswers];
newAnswers[part.index!] = e.target.value;
setUserAnswers(newAnswers);
}}
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"
}`}
style={{ width: `${Math.max((part.answer?.length || 5) * 12, 60)}px` }}
placeholder="..."
/>
)
))}
</div>
{!submitted && parsed.answers.length > 0 && (
<button
onClick={() => setSubmitted(true)}
className="btn-premium w-full py-4 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-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
>
Try Again
</button>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,175 @@
"use client";
import { useState, useMemo } from "react";
interface MatchingPair {
left: string;
right: string;
}
interface MatchingBlockProps {
id: string;
title?: string;
pairs: MatchingPair[];
editMode: boolean;
onChange: (updates: { title?: string; pairs?: MatchingPair[] }) => void;
}
export default function MatchingBlock({ id, title, pairs, editMode, onChange }: MatchingBlockProps) {
const [selectedLeft, setSelectedLeft] = useState<number | null>(null);
const [matches, setMatches] = useState<Record<number, number>>({}); // leftIdx -> rightIdx
const [submitted, setSubmitted] = useState(false);
// Shuffled right items for the game
const shuffledRight = useMemo(() => {
return pairs
.map((p, i) => ({ value: p.right, originalIdx: i }))
.sort(() => Math.random() - 0.5);
}, [pairs]);
const handleMatch = (leftIdx: number, rightIdx: number) => {
if (submitted) return;
setMatches(prev => ({ ...prev, [leftIdx]: rightIdx }));
setSelectedLeft(null);
};
const handleReset = () => {
setSubmitted(false);
setMatches({});
setSelectedLeft(null);
};
return (
<div className="space-y-8" id={id}>
<div className="space-y-2">
{editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Section Title (Optional)</label>
<input
type="text"
value={title || ""}
onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Match the concepts..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none"
/>
</div>
) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
{title || "Concept Matching"}
</h3>
)}
</div>
{editMode ? (
<div className="space-y-4">
{pairs.map((pair, idx) => (
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300">
<input
value={pair.left}
onChange={(e) => {
const newPairs = [...pairs];
newPairs[idx].left = e.target.value;
onChange({ pairs: newPairs });
}}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
placeholder="Term A"
/>
<span className="text-gray-500 font-bold"></span>
<input
value={pair.right}
onChange={(e) => {
const newPairs = [...pairs];
newPairs[idx].right = e.target.value;
onChange({ pairs: newPairs });
}}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
placeholder="Definition B"
/>
<button
onClick={() => {
const newPairs = pairs.filter((_, i) => i !== idx);
onChange({ pairs: newPairs });
}}
className="p-2 text-gray-500 hover:text-red-400"
>
×
</button>
</div>
))}
<button
onClick={() => onChange({ pairs: [...pairs, { left: "", right: "" }] })}
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl"
>
+ Add Pair
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-8 glass border-white/5 rounded-3xl relative">
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Term</label>
{pairs.map((pair, i) => (
<button
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"
}`}
>
{pair.left}
</button>
))}
</div>
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Definition</label>
{shuffledRight.map((item, i) => {
const matchedLeftIdx = Object.keys(matches).find(k => matches[parseInt(k)] === item.originalIdx);
const isCorrect = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) === item.originalIdx;
const isWrong = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) !== item.originalIdx;
return (
<button
key={i}
disabled={selectedLeft === null || submitted}
onClick={() => handleMatch(selectedLeft!, item.originalIdx)}
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white/5" : ""
} ${isCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
isWrong ? "border-red-500 bg-red-500/20 text-red-100" :
matchedLeftIdx !== undefined ? "border-blue-500/30 bg-blue-500/5 text-blue-400" :
"border-white/5 bg-white/5 text-gray-200"
}`}
>
<div className="flex items-center justify-between">
<span>{item.value}</span>
{isCorrect && <span></span>}
{isWrong && <span></span>}
</div>
</button>
);
})}
</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-4 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-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
>
Try Again
</button>
)}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,192 @@
"use client";
import { useState, useMemo } from "react";
interface OrderingBlockProps {
id: string;
title?: string;
items: string[];
editMode: boolean;
onChange: (updates: { title?: string; items?: string[] }) => void;
}
export default function OrderingBlock({ id, title, items, editMode, onChange }: OrderingBlockProps) {
const [userOrder, setUserOrder] = useState<number[]>([]); // Array of original indices in user-selected order
const [submitted, setSubmitted] = useState(false);
// Shuffled items for the start
const shuffledItems = useMemo(() => {
return items
.map((item, i) => ({ value: item, originalIdx: i }))
.sort(() => Math.random() - 0.5);
}, [items]);
const handlePick = (originalIdx: number) => {
if (submitted) return;
if (userOrder.includes(originalIdx)) {
setUserOrder(userOrder.filter(i => i !== originalIdx));
} else {
setUserOrder([...userOrder, originalIdx]);
}
};
const handleReset = () => {
setSubmitted(false);
setUserOrder([]);
};
return (
<div className="space-y-8" id={id}>
<div className="space-y-2">
{editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Section Title (Optional)</label>
<input
type="text"
value={title || ""}
onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Sequence of Events..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none"
/>
</div>
) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
{title || "Sequence Ordering"}
</h3>
)}
</div>
{editMode ? (
<div className="space-y-4">
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-2">Define items in their CORRECT order:</p>
{items.map((item, idx) => (
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300">
<span className="text-blue-500 font-black w-6">{idx + 1}.</span>
<input
value={item}
onChange={(e) => {
const newItems = [...items];
newItems[idx] = e.target.value;
onChange({ items: newItems });
}}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
placeholder={`Step ${idx + 1}`}
/>
<div className="flex gap-2">
<button
disabled={idx === 0}
onClick={() => {
const newItems = [...items];
[newItems[idx], newItems[idx - 1]] = [newItems[idx - 1], newItems[idx]];
onChange({ items: newItems });
}}
className="p-2 text-gray-500 hover:text-white disabled:opacity-20"
>
</button>
<button
disabled={idx === items.length - 1}
onClick={() => {
const newItems = [...items];
[newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]];
onChange({ items: newItems });
}}
className="p-2 text-gray-500 hover:text-white disabled:opacity-20"
>
</button>
</div>
<button
onClick={() => {
const newItems = items.filter((_, i) => i !== idx);
onChange({ items: newItems });
}}
className="p-2 text-gray-500 hover:text-red-400"
>
×
</button>
</div>
))}
<button
onClick={() => onChange({ items: [...items, ""] })}
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl"
>
+ Add Step
</button>
</div>
) : (
<div className="space-y-8 p-8 glass border-white/5 rounded-3xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Available Items</label>
<div className="flex flex-wrap gap-3">
{shuffledItems.map((item, i) => {
const isPicked = userOrder.includes(item.originalIdx);
return (
<button
key={i}
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"
}`}
>
{item.value}
</button>
);
})}
</div>
</div>
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Your Sequence</label>
<div className="space-y-3">
{userOrder.length === 0 && <p className="text-xs text-gray-600 italic py-4">Click items to build the sequence...</p>}
{userOrder.map((idx, i) => {
const isItemCorrect = submitted && idx === i;
const isItemWrong = submitted && idx !== i;
return (
<div
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"
}`}
>
<span className="opacity-50 text-xs">{i + 1}.</span>
<span className="flex-1">{items[idx]}</span>
{!submitted && <span className="text-xs opacity-50">×</span>}
{isItemCorrect && <span></span>}
{isItemWrong && <span></span>}
</div>
);
})}
</div>
</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-4 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-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
>
Try Again
</button>
)}
</div>
</div>
)}
</div>
);
}
+64 -25
View File
@@ -6,8 +6,8 @@ interface QuizQuestion {
id: string;
question: string;
options: string[];
correct: number;
type?: 'multiple-choice' | 'true-false';
correct: number[];
type?: 'multiple-choice' | 'true-false' | 'multiple-select';
}
interface QuizBlockProps {
@@ -17,11 +17,11 @@ interface QuizBlockProps {
questions: QuizQuestion[];
};
editMode: boolean;
onChange: (data: { title?: string; questions?: QuizQuestion[] }) => void;
onChange: (data: { title?: string; quiz_data?: { questions: QuizQuestion[] } }) => void;
}
export default function QuizBlock({ id, title, quizData, editMode, onChange }: QuizBlockProps) {
const [userAnswers, setUserAnswers] = useState<Record<string, number>>({});
const [userAnswers, setUserAnswers] = useState<Record<string, number[]>>({});
const [submitted, setSubmitted] = useState(false);
const questions = quizData.questions || [];
@@ -31,21 +31,43 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
id: Math.random().toString(36).substr(2, 9),
question: "New Question?",
options: ["Option 1", "Option 2"],
correct: 0,
correct: [0],
type: 'multiple-choice'
};
onChange({ questions: [...questions, newQuestion] });
onChange({ quiz_data: { questions: [...questions, newQuestion] } });
};
const updateQuestion = (index: number, updates: Partial<QuizQuestion>) => {
const newQuestions = [...questions];
newQuestions[index] = { ...newQuestions[index], ...updates };
onChange({ questions: newQuestions });
onChange({ quiz_data: { questions: newQuestions } });
};
const handleAnswer = (qId: string, optionIndex: number) => {
const toggleCorrectOption = (qIdx: number, optIdx: number, isMulti: boolean) => {
const current = questions[qIdx].correct || [];
if (isMulti) {
const next = current.includes(optIdx)
? current.filter(i => i !== optIdx)
: [...current, optIdx].sort((a, b) => a - b);
updateQuestion(qIdx, { correct: next.length ? next : [0] });
} else {
updateQuestion(qIdx, { correct: [optIdx] });
}
};
const handleAnswer = (qId: string, optionIndex: number, isMulti: boolean) => {
if (submitted) return;
setUserAnswers(prev => ({ ...prev, [qId]: optionIndex }));
setUserAnswers(prev => {
const current = prev[qId] || [];
if (isMulti) {
const next = current.includes(optionIndex)
? current.filter(i => i !== optionIndex)
: [...current, optionIndex].sort((a, b) => a - b);
return { ...prev, [qId]: next };
} else {
return { ...prev, [qId]: [optionIndex] };
}
});
};
return (
@@ -77,13 +99,19 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
<div className="flex items-center justify-between gap-4">
<div className="flex bg-white/5 rounded-lg p-1 border border-white/5">
<button
onClick={() => updateQuestion(idx, { type: 'multiple-choice' })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type !== 'true-false' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
onClick={() => updateQuestion(idx, { type: 'multiple-choice', correct: [q.correct?.[0] || 0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-choice' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
>
MCQ
</button>
<button
onClick={() => updateQuestion(idx, { type: 'true-false', options: ["True", "False"], correct: 0 })}
onClick={() => updateQuestion(idx, { type: 'multiple-select', correct: [q.correct?.[0] || 0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-select' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
>
MSQ
</button>
<button
onClick={() => updateQuestion(idx, { type: 'true-false', options: ["True", "False"], correct: [0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'true-false' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
>
T / F
@@ -92,7 +120,7 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
<button
onClick={() => {
const newQuestions = questions.filter((_, i) => i !== idx);
onChange({ questions: newQuestions });
onChange({ quiz_data: { questions: newQuestions } });
}}
className="p-2 text-gray-500 hover:text-red-400 transition-colors"
>
@@ -113,8 +141,8 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
{["True", "False"].map((opt, oIdx) => (
<button
key={oIdx}
onClick={() => updateQuestion(idx, { correct: oIdx })}
className={`flex-1 py-4 rounded-xl border-2 transition-all font-black text-xs uppercase tracking-widest ${q.correct === oIdx ? "border-blue-500 bg-blue-500/10 text-white" : "border-white/5 bg-white/5 text-gray-500"}`}
onClick={() => updateQuestion(idx, { correct: [oIdx] })}
className={`flex-1 py-4 rounded-xl border-2 transition-all font-black text-xs uppercase tracking-widest ${q.correct?.includes(oIdx) ? "border-blue-500 bg-blue-500/10 text-white" : "border-white/5 bg-white/5 text-gray-500"}`}
>
{opt}
</button>
@@ -124,9 +152,9 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
q.options.map((opt, oIdx) => (
<div key={oIdx} className="flex gap-3 items-center group/opt">
<input
type="radio"
checked={q.correct === oIdx}
onChange={() => updateQuestion(idx, { correct: oIdx })}
type={q.type === 'multiple-select' ? "checkbox" : "radio"}
checked={q.correct?.includes(oIdx)}
onChange={() => toggleCorrectOption(idx, oIdx, q.type === 'multiple-select')}
className="w-5 h-5 accent-blue-500 cursor-pointer"
/>
<input
@@ -143,7 +171,8 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
<button
onClick={() => {
const newOpts = q.options.filter((_, i) => i !== oIdx);
updateQuestion(idx, { options: newOpts, correct: q.correct >= newOpts.length ? 0 : q.correct });
const newCorrect = q.correct?.filter(i => i !== oIdx).map(i => i > oIdx ? i - 1 : i);
updateQuestion(idx, { options: newOpts, correct: newCorrect?.length ? newCorrect : [0] });
}}
className="opacity-0 group-hover/opt:opacity-100 p-2 text-gray-500 hover:text-red-400 transition-all"
>
@@ -179,12 +208,17 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
<h4 className="font-bold text-xl text-gray-100 leading-tight">{q.question}</h4>
<div className="grid gap-3">
{q.options.map((opt, oIdx) => {
const isSelected = userAnswers[q.id] === oIdx;
const isCorrect = q.correct === oIdx;
const isSelected = userAnswers[q.id]?.includes(oIdx);
const isCorrect = q.correct?.includes(oIdx);
const isActuallyCorrect = isCorrect && isSelected;
const isWrongSelection = !isCorrect && isSelected;
const missedCorrect = isCorrect && !isSelected;
let style = "glass border-white/10 hover:bg-white/5";
if (submitted) {
if (isCorrect) style = "bg-green-500/20 border-green-500 text-green-400";
else if (isSelected && !isCorrect) style = "bg-red-500/20 border-red-500 text-red-100";
if (isActuallyCorrect) style = "bg-green-500/20 border-green-500 text-green-400";
else if (isWrongSelection) style = "bg-red-500/20 border-red-500 text-red-100";
else if (missedCorrect) style = "border-orange-500/50 text-orange-400 animate-pulse";
else style = "opacity-50 grayscale border-white/5";
} else if (isSelected) {
style = "bg-blue-500/20 border-blue-500 text-white shadow-[0_0_20px_rgba(59,130,246,0.2)]";
@@ -193,10 +227,15 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
return (
<button
key={oIdx}
onClick={() => handleAnswer(q.id, oIdx)}
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold ${style}`}
>
{opt}
<div className="flex items-center justify-between">
<span>{opt}</span>
{submitted && isActuallyCorrect && <span></span>}
{submitted && isWrongSelection && <span></span>}
{submitted && missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter">Correct Answer</span>}
</div>
</button>
);
})}
@@ -0,0 +1,116 @@
"use client";
import { useState } from "react";
interface ShortAnswerBlockProps {
id: string;
title?: string;
prompt: string;
correctAnswers: string[];
editMode: boolean;
onChange: (updates: { title?: string; prompt?: string; correctAnswers?: string[] }) => void;
}
export default function ShortAnswerBlock({ id, title, prompt, correctAnswers, editMode, onChange }: ShortAnswerBlockProps) {
const [userAnswer, setUserAnswer] = useState("");
const [submitted, setSubmitted] = useState(false);
const handleReset = () => {
setSubmitted(false);
setUserAnswer("");
};
const isCorrect = correctAnswers.some(ans => ans.trim().toLowerCase() === userAnswer.trim().toLowerCase());
return (
<div className="space-y-8" id={id}>
<div className="space-y-2">
{editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Section Title (Optional)</label>
<input
type="text"
value={title || ""}
onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Critical Thinking, Quick Response..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none"
/>
</div>
) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
{title || "Short Answer"}
</h3>
)}
</div>
{editMode ? (
<div className="space-y-6">
<div className="p-6 glass border-white/5 space-y-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Question Prompt</label>
<textarea
value={prompt}
onChange={(e) => onChange({ prompt: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all"
placeholder="Type the question for the student..."
/>
</div>
<div className="p-6 glass border-white/5 space-y-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Correct Answers (One per line)</label>
<textarea
value={correctAnswers ? correctAnswers.join("\n") : ""}
onChange={(e) => onChange({ correctAnswers: e.target.value.split("\n").filter(a => a.trim() !== "") })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-sm font-medium focus:outline-none focus:border-blue-500/50 transition-all"
placeholder="Answer 1&#10;Answer 2 (Alternative)"
/>
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Validation is case-insensitive. Provide multiple alternatives if necessary.</p>
</div>
</div>
) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
<p className="text-xl font-bold text-gray-100">{prompt || "Please enter your answer below:"}</p>
<div className="space-y-4">
<input
type="text"
value={userAnswer}
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"
}`}
placeholder="Type your answer..."
/>
{submitted && !isCorrect && (
<div className="p-4 bg-orange-500/10 border border-orange-500/20 rounded-xl">
<p className="text-[10px] text-orange-400 uppercase font-black tracking-widest">Suggested Answer(s):</p>
<p className="text-sm text-gray-400 mt-1">{correctAnswers && correctAnswers[0]}</p>
</div>
)}
</div>
{!submitted && (
<button
onClick={() => setSubmitted(true)}
disabled={!userAnswer.trim()}
className="btn-premium w-full py-4 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-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
>
Try Again
</button>
)}
</div>
)}
</div>
);
}