Finished implementing a Solutions version for each exercise
This commit is contained in:
@@ -15,8 +15,6 @@ interface WordsPopoutProps {
|
||||
onAnswer: (answer: string) => void;
|
||||
}
|
||||
|
||||
type UserSolution = {id: string; solution: string};
|
||||
|
||||
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
@@ -72,10 +70,17 @@ function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function FillBlanks({allowRepetition, prompt, solutions, text, words, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||
export default function FillBlanks({id, allowRepetition, prompt, solutions, text, words, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||
const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]);
|
||||
const [currentBlankId, setCurrentBlankId] = useState<string>();
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter((x) => solutions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||
|
||||
return {total, correct};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
@@ -123,7 +128,9 @@ export default function FillBlanks({allowRepetition, prompt, solutions, text, wo
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => onNext({id, solutions: userSolutions, score: calculateScore()})}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
@@ -133,59 +140,3 @@ export default function FillBlanks({allowRepetition, prompt, solutions, text, wo
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FillBlanksSolutions({
|
||||
allowRepetition,
|
||||
prompt,
|
||||
solutions,
|
||||
text,
|
||||
words,
|
||||
userSolutions,
|
||||
}: FillBlanksExercise & {userSolutions: UserSolution[]}) {
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
{reactStringReplace(line, /({{\d}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = userSolutions.find((x) => x.id === id);
|
||||
const solution = solutions.find((x) => x.id === id)!;
|
||||
|
||||
if (!userSolution) {
|
||||
return (
|
||||
<>
|
||||
<button className={clsx("border-2 rounded-xl px-4 text-gray-500 border-gray-500")}>{solution.solution}</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userSolution.solution === solution.solution) {
|
||||
return <button className={clsx("border-2 rounded-xl px-4 text-green-500 border-green-500")}>{solution.solution}</button>;
|
||||
}
|
||||
|
||||
if (userSolution.solution !== solution.solution) {
|
||||
return (
|
||||
<>
|
||||
<button className={clsx("border-2 rounded-xl px-4 text-red-500 border-red-500 mr-1")}>{userSolution.solution}</button>
|
||||
<button className={clsx("border-2 rounded-xl px-4 text-blue-400 border-blue-400")}>{solution.solution}</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,17 @@ import {useState} from "react";
|
||||
import LineTo from "react-lineto";
|
||||
import {CommonProps} from ".";
|
||||
|
||||
const AVAILABLE_COLORS = ["#63526a", "#f7651d", "#278f04", "#ef4487", "#ca68c0", "#f5fe9b", "#b3ab01", "#af963a", "#9a85f1", "#1b1750"];
|
||||
|
||||
export default function MatchSentences({allowRepetition, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||
export default function MatchSentences({id, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
||||
const [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = sentences.length;
|
||||
const correct = userSolutions.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
|
||||
return {total, correct};
|
||||
};
|
||||
|
||||
const selectOption = (option: string) => {
|
||||
if (!selectedQuestion) return;
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]);
|
||||
@@ -87,7 +92,9 @@ export default function MatchSentences({allowRepetition, options, prompt, senten
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => onNext({id, solutions: userSolutions, score: calculateScore()})}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
@@ -97,106 +104,3 @@ export default function MatchSentences({allowRepetition, options, prompt, senten
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function MatchSentencesSolutions({allowRepetition, options, prompt, sentences, userSolutions}: MatchSentencesExercise) {
|
||||
const getSentenceColor = (id: string) => {
|
||||
return sentences.find((x) => x.id === id)?.color || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<div className="grid grid-cols-2 gap-16 place-items-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{sentences.map(({sentence, id, color}) => (
|
||||
<div key={`question_${id}`} className="flex items-center justify-end gap-2 cursor-pointer">
|
||||
<span>
|
||||
<span className="font-semibold">{id}.</span> {sentence}{" "}
|
||||
</span>
|
||||
<div
|
||||
style={{borderColor: color, backgroundColor: "transparent"}}
|
||||
className={clsx("border-2 border-blue-500 w-4 h-4 rounded-full", id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{options.map(({sentence, id}) => (
|
||||
<div key={`answer_${id}`} className={clsx("flex items-center justify-start gap-2 cursor-pointer")}>
|
||||
<div
|
||||
style={
|
||||
userSolutions.find((x) => x.option === id)
|
||||
? {
|
||||
border: `2px solid ${getSentenceColor(userSolutions.find((x) => x.option === id)!.question)}`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
className={clsx("border-2 border-green-500 bg-transparent w-4 h-4 rounded-full", id)}
|
||||
/>
|
||||
<span>
|
||||
<span className="font-semibold">{id}.</span> {sentence}{" "}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{userSolutions.map((solution, index) => (
|
||||
<div key={`solution_${index}`} className="absolute">
|
||||
<LineTo
|
||||
className="rounded-full"
|
||||
from={solution.question}
|
||||
to={solution.option}
|
||||
borderColor={sentences.find((x) => x.id === solution.question)!.color}
|
||||
borderWidth={5}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-16 place-items-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{sentences.map(({sentence, id, color}) => (
|
||||
<div key={`question_${id}`} className="flex items-center justify-end gap-2 cursor-pointer">
|
||||
<span>
|
||||
<span className="font-semibold">{id}.</span> {sentence}{" "}
|
||||
</span>
|
||||
<div
|
||||
style={{borderColor: color, backgroundColor: "transparent"}}
|
||||
className={clsx("border-2 border-blue-500 w-4 h-4 rounded-full", id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{options.map(({sentence, id}) => (
|
||||
<div key={`answer_${id}`} className={clsx("flex items-center justify-start gap-2 cursor-pointer")}>
|
||||
<div
|
||||
style={
|
||||
userSolutions.find((x) => x.option === id)
|
||||
? {
|
||||
border: `2px solid ${getSentenceColor(userSolutions.find((x) => x.option === id)!.question)}`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
className={clsx("border-2 border-green-500 bg-transparent w-4 h-4 rounded-full", id)}
|
||||
/>
|
||||
<span>
|
||||
<span className="font-semibold">{id}.</span> {sentence}{" "}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{sentences.map(({id, solution}, index) => (
|
||||
<div key={`solution_${index}`} className="absolute">
|
||||
<LineTo
|
||||
className="rounded-full"
|
||||
from={id}
|
||||
to={solution}
|
||||
borderColor={sentences.find((x) => x.id === id)!.color}
|
||||
borderWidth={5}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ function Question({
|
||||
);
|
||||
}
|
||||
|
||||
export default function MultipleChoice({prompt, questions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
||||
export default function MultipleChoice({id, prompt, questions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
||||
const [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]);
|
||||
const [questionIndex, setQuestionIndex] = useState(0);
|
||||
|
||||
@@ -92,9 +92,16 @@ export default function MultipleChoice({prompt, questions, onNext, onBack}: Mult
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
|
||||
};
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = questions.length;
|
||||
const correct = userSolutions.filter((x) => questions.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
|
||||
return {total, correct};
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (questionIndex === questions.length - 1) {
|
||||
onNext();
|
||||
onNext({id, solutions: userSolutions, score: calculateScore()});
|
||||
} else {
|
||||
setQuestionIndex((prev) => prev + 1);
|
||||
}
|
||||
@@ -137,52 +144,3 @@ export default function MultipleChoice({prompt, questions, onNext, onBack}: Mult
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function MultipleChoiceSolutions({prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
||||
const [questionIndex, setQuestionIndex] = useState(0);
|
||||
|
||||
const next = () => {
|
||||
if (questionIndex === questions.length - 1) {
|
||||
onNext();
|
||||
} else {
|
||||
setQuestionIndex((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
if (questionIndex === 0) {
|
||||
onBack();
|
||||
} else {
|
||||
setQuestionIndex((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
{questionIndex < questions.length && (
|
||||
<Question
|
||||
{...questions[questionIndex]}
|
||||
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
|
||||
showSolution
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="self-end flex gap-8">
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={back}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={next}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,51 +11,53 @@ import {toast} from "react-toastify";
|
||||
function Blank({
|
||||
id,
|
||||
maxWords,
|
||||
solutions,
|
||||
userSolution,
|
||||
disabled = false,
|
||||
showSolutions = false,
|
||||
setUserSolution,
|
||||
}: {
|
||||
id: string;
|
||||
solutions?: string[];
|
||||
userSolution?: string;
|
||||
maxWords: number;
|
||||
disabled?: boolean;
|
||||
showSolutions?: boolean;
|
||||
setUserSolution?: (solution: string) => void;
|
||||
}) {
|
||||
const [userInput, setUserInput] = useState(userSolution || "");
|
||||
const [userInput, setUserInput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const words = userInput.split(" ").filter((x) => x !== "");
|
||||
if (words.length >= maxWords) {
|
||||
toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"});
|
||||
setUserInput(words.join(" ").trim());
|
||||
if (setUserSolution) setUserSolution(words.join(" ").trim());
|
||||
}
|
||||
}, [maxWords, userInput, setUserSolution]);
|
||||
|
||||
const getSolutionStyling = () => {
|
||||
if (solutions && userSolution) {
|
||||
if (solutions.map((x) => x.trim().toLowerCase()).includes(userSolution.trim().toLowerCase())) return "text-green-500 border-green-500";
|
||||
}
|
||||
|
||||
return "text-red-500 border-red-500";
|
||||
};
|
||||
}, [maxWords, userInput]);
|
||||
|
||||
return (
|
||||
<input
|
||||
className={clsx("input border rounded-xl px-2 py-1 bg-white", !solutions && "text-blue-400 border-blue-400", getSolutionStyling())}
|
||||
className={clsx("input border rounded-xl px-2 py-1 bg-white text-blue-400 border-blue-400")}
|
||||
placeholder={id}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
value={!solutions ? userInput : solutions.join(" / ")}
|
||||
contentEditable={disabled}
|
||||
value={userInput}
|
||||
contentEditable={showSolutions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WriteBlanks({prompt, maxWords, solutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
||||
export default function WriteBlanks({id, prompt, maxWords, solutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
||||
const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter(
|
||||
(x) =>
|
||||
solutions
|
||||
.find((y) => x.id === y.id)
|
||||
?.solution.map((y) => y.toLowerCase())
|
||||
.includes(x.solution.toLowerCase()) || false,
|
||||
).length;
|
||||
|
||||
return {total, correct};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
@@ -63,6 +65,7 @@ export default function WriteBlanks({prompt, maxWords, solutions, text, onNext,
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = userSolutions.find((x) => x.id === id);
|
||||
const setUserSolution = (solution: string) => {
|
||||
console.log({solution});
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.id !== id), {id, solution}]);
|
||||
};
|
||||
|
||||
@@ -93,54 +96,9 @@ export default function WriteBlanks({prompt, maxWords, solutions, text, onNext,
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WriteBlanksSolutions({prompt, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = userSolutions.find((x) => x.id === id);
|
||||
const solution = solutions.find((x) => x.id === id)!;
|
||||
|
||||
return <Blank userSolution={userSolution?.solution} maxWords={maxWords} id={id} solutions={solution.solution} disabled />;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex gap-8">
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={onBack}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => onNext({id, solutions: userSolutions, score: calculateScore()})}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, WriteBlanksExercise} from "@/interfaces/exam";
|
||||
import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, UserSolution, WriteBlanksExercise} from "@/interfaces/exam";
|
||||
import dynamic from "next/dynamic";
|
||||
import FillBlanks from "./FillBlanks";
|
||||
import MultipleChoice from "./MultipleChoice";
|
||||
@@ -7,11 +7,11 @@ import WriteBlanks from "./WriteBlanks";
|
||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
||||
|
||||
export interface CommonProps {
|
||||
onNext: () => void;
|
||||
onNext: (userSolutions: UserSolution) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const renderExercise = (exercise: Exercise, onNext: () => void, onBack: () => void) => {
|
||||
export const renderExercise = (exercise: Exercise, onNext: (userSolutions: UserSolution) => void, onBack: () => void) => {
|
||||
switch (exercise.type) {
|
||||
case "fillBlanks":
|
||||
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||
|
||||
Reference in New Issue
Block a user