Finished implementing a Solutions version for each exercise
This commit is contained in:
@@ -15,8 +15,6 @@ interface WordsPopoutProps {
|
|||||||
onAnswer: (answer: string) => void;
|
onAnswer: (answer: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSolution = {id: string; solution: string};
|
|
||||||
|
|
||||||
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<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 [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]);
|
||||||
const [currentBlankId, setCurrentBlankId] = useState<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) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@@ -123,7 +128,9 @@ export default function FillBlanks({allowRepetition, prompt, solutions, text, wo
|
|||||||
</div>
|
</div>
|
||||||
Back
|
Back
|
||||||
</button>
|
</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
|
Next
|
||||||
<div className="absolute right-4">
|
<div className="absolute right-4">
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
<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 LineTo from "react-lineto";
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
|
|
||||||
const AVAILABLE_COLORS = ["#63526a", "#f7651d", "#278f04", "#ef4487", "#ca68c0", "#f5fe9b", "#b3ab01", "#af963a", "#9a85f1", "#1b1750"];
|
export default function MatchSentences({id, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||||
|
|
||||||
export default function MatchSentences({allowRepetition, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
|
||||||
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
||||||
const [userSolutions, setUserSolutions] = useState<{question: string; option: 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) => {
|
const selectOption = (option: string) => {
|
||||||
if (!selectedQuestion) return;
|
if (!selectedQuestion) return;
|
||||||
setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]);
|
setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]);
|
||||||
@@ -87,7 +92,9 @@ export default function MatchSentences({allowRepetition, options, prompt, senten
|
|||||||
</div>
|
</div>
|
||||||
Back
|
Back
|
||||||
</button>
|
</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
|
Next
|
||||||
<div className="absolute right-4">
|
<div className="absolute right-4">
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
<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 [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]);
|
||||||
const [questionIndex, setQuestionIndex] = useState(0);
|
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}]);
|
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 = () => {
|
const next = () => {
|
||||||
if (questionIndex === questions.length - 1) {
|
if (questionIndex === questions.length - 1) {
|
||||||
onNext();
|
onNext({id, solutions: userSolutions, score: calculateScore()});
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex((prev) => prev + 1);
|
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({
|
function Blank({
|
||||||
id,
|
id,
|
||||||
maxWords,
|
maxWords,
|
||||||
solutions,
|
showSolutions = false,
|
||||||
userSolution,
|
|
||||||
disabled = false,
|
|
||||||
setUserSolution,
|
setUserSolution,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
solutions?: string[];
|
solutions?: string[];
|
||||||
userSolution?: string;
|
userSolution?: string;
|
||||||
maxWords: number;
|
maxWords: number;
|
||||||
disabled?: boolean;
|
showSolutions?: boolean;
|
||||||
setUserSolution?: (solution: string) => void;
|
setUserSolution?: (solution: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [userInput, setUserInput] = useState(userSolution || "");
|
const [userInput, setUserInput] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const words = userInput.split(" ").filter((x) => x !== "");
|
const words = userInput.split(" ").filter((x) => x !== "");
|
||||||
if (words.length >= maxWords) {
|
if (words.length >= maxWords) {
|
||||||
toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"});
|
toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"});
|
||||||
setUserInput(words.join(" ").trim());
|
setUserInput(words.join(" ").trim());
|
||||||
if (setUserSolution) setUserSolution(words.join(" ").trim());
|
|
||||||
}
|
}
|
||||||
}, [maxWords, userInput, setUserSolution]);
|
}, [maxWords, userInput]);
|
||||||
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<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}
|
placeholder={id}
|
||||||
onChange={(e) => setUserInput(e.target.value)}
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
value={!solutions ? userInput : solutions.join(" / ")}
|
value={userInput}
|
||||||
contentEditable={disabled}
|
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 [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) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@@ -63,6 +65,7 @@ export default function WriteBlanks({prompt, maxWords, solutions, text, onNext,
|
|||||||
const id = match.replaceAll(/[\{\}]/g, "");
|
const id = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = userSolutions.find((x) => x.id === id);
|
const userSolution = userSolutions.find((x) => x.id === id);
|
||||||
const setUserSolution = (solution: string) => {
|
const setUserSolution = (solution: string) => {
|
||||||
|
console.log({solution});
|
||||||
setUserSolutions((prev) => [...prev.filter((x) => x.id !== id), {id, 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>
|
</div>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
<button
|
||||||
Next
|
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||||
<div className="absolute right-4">
|
onClick={() => onNext({id, solutions: userSolutions, score: calculateScore()})}>
|
||||||
<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}>
|
|
||||||
Next
|
Next
|
||||||
<div className="absolute right-4">
|
<div className="absolute right-4">
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
<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 dynamic from "next/dynamic";
|
||||||
import FillBlanks from "./FillBlanks";
|
import FillBlanks from "./FillBlanks";
|
||||||
import MultipleChoice from "./MultipleChoice";
|
import MultipleChoice from "./MultipleChoice";
|
||||||
@@ -7,11 +7,11 @@ import WriteBlanks from "./WriteBlanks";
|
|||||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
||||||
|
|
||||||
export interface CommonProps {
|
export interface CommonProps {
|
||||||
onNext: () => void;
|
onNext: (userSolutions: UserSolution) => void;
|
||||||
onBack: () => 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) {
|
switch (exercise.type) {
|
||||||
case "fillBlanks":
|
case "fillBlanks":
|
||||||
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
|||||||
73
src/components/Solutions/FillBlanks.tsx
Normal file
73
src/components/Solutions/FillBlanks.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import {FillBlanksExercise} from "@/interfaces/exam";
|
||||||
|
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
import {CommonProps} from ".";
|
||||||
|
|
||||||
|
export default function FillBlanksSolutions({prompt, solutions, text, userSolutions, onNext, onBack}: FillBlanksExercise & 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)!;
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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()}>
|
||||||
|
Next
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
src/components/Solutions/MatchSentences.tsx
Normal file
73
src/components/Solutions/MatchSentences.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {MatchSentencesExercise} from "@/interfaces/exam";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import LineTo from "react-lineto";
|
||||||
|
import {CommonProps} from ".";
|
||||||
|
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
|
||||||
|
export default function MatchSentencesSolutions({options, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||||
|
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, solution}) => (
|
||||||
|
<div
|
||||||
|
key={`question_${id}`}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-end gap-2 cursor-pointer",
|
||||||
|
userSolutions.find((x) => x.question === id)?.option === solution ? "text-green-500" : "text-red-500",
|
||||||
|
)}>
|
||||||
|
<span>
|
||||||
|
<span className="font-semibold">{id}.</span> {sentence}{" "}
|
||||||
|
</span>
|
||||||
|
<div style={{borderColor: color}} 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={
|
||||||
|
sentences.find((x) => x.solution === id)
|
||||||
|
? {
|
||||||
|
border: `2px solid ${sentences.find((x) => x.solution === id)!.color}`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
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((sentence, index) => (
|
||||||
|
<div key={`solution_${index}`} className="absolute">
|
||||||
|
<LineTo className="rounded-full" from={sentence.id} to={sentence.solution} borderColor={sentence.color} borderWidth={5} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</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()}>
|
||||||
|
Next
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/components/Solutions/MultipleChoice.tsx
Normal file
133
src/components/Solutions/MultipleChoice.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
|
||||||
|
import {mdiArrowLeft, mdiArrowRight, mdiCheck, mdiClose} from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {CommonProps} from ".";
|
||||||
|
|
||||||
|
function Question({
|
||||||
|
variant,
|
||||||
|
prompt,
|
||||||
|
solution,
|
||||||
|
options,
|
||||||
|
userSolution,
|
||||||
|
onSelectOption,
|
||||||
|
showSolution = false,
|
||||||
|
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
||||||
|
const optionColor = (option: string) => {
|
||||||
|
if (!showSolution) {
|
||||||
|
return userSolution === option ? "border-blue-400" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option === solution) {
|
||||||
|
return "border-green-500 text-green-500";
|
||||||
|
}
|
||||||
|
|
||||||
|
return userSolution === option ? "border-red-500 text-red-500" : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionBadge = (option: string) => {
|
||||||
|
if (option === userSolution) {
|
||||||
|
if (solution === option) {
|
||||||
|
return (
|
||||||
|
<div className="badge badge-lg bg-green-500 border-green-500 absolute -top-2 -right-4">
|
||||||
|
<div className="tooltip" data-tip="You have correctly answered!">
|
||||||
|
<Icon path={mdiCheck} color="white" size={0.8} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="badge badge-lg bg-red-500 border-red-500 absolute -top-2 -right-4">
|
||||||
|
<div className="tooltip" data-tip="You have wrongly answered!">
|
||||||
|
<Icon path={mdiClose} color="white" size={0.8} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<span>{prompt}</span>
|
||||||
|
<div className="grid grid-cols-4 gap-4 place-items-center">
|
||||||
|
{variant === "image" &&
|
||||||
|
options.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => (onSelectOption ? onSelectOption(option.id) : null)}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-center border-2 p-4 rounded-xl gap-4 cursor-pointer bg-white relative",
|
||||||
|
optionColor(option.id),
|
||||||
|
)}>
|
||||||
|
{showSolution && optionBadge(option.id)}
|
||||||
|
<img src={option.src!} alt={`Option ${option.id}`} />
|
||||||
|
<span>{option.id}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{variant === "text" &&
|
||||||
|
options.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => (onSelectOption ? onSelectOption(option.id) : null)}
|
||||||
|
className={clsx("flex border-2 p-4 rounded-xl gap-2 cursor-pointer bg-white", optionColor(option.id))}>
|
||||||
|
<span className="font-bold">{option.id}.</span>
|
||||||
|
<span>{option.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MultipleChoice({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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/components/Solutions/WriteBlanks.tsx
Normal file
110
src/components/Solutions/WriteBlanks.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import {WriteBlanksExercise} from "@/interfaces/exam";
|
||||||
|
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
import {CommonProps} from ".";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
function Blank({
|
||||||
|
id,
|
||||||
|
maxWords,
|
||||||
|
solutions,
|
||||||
|
userSolution,
|
||||||
|
disabled = false,
|
||||||
|
setUserSolution,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
solutions?: string[];
|
||||||
|
userSolution?: string;
|
||||||
|
maxWords: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
setUserSolution?: (solution: string) => void;
|
||||||
|
}) {
|
||||||
|
const [userInput, setUserInput] = useState(userSolution || "");
|
||||||
|
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={clsx("input border rounded-xl px-2 py-1 bg-white", !solutions && "text-blue-400 border-blue-400", getSolutionStyling())}
|
||||||
|
placeholder={id}
|
||||||
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
|
value={!solutions ? userInput : solutions.join(" / ")}
|
||||||
|
contentEditable={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WriteBlanksSolutions({
|
||||||
|
id,
|
||||||
|
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()}>
|
||||||
|
Next
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/Solutions/index.tsx
Normal file
25
src/components/Solutions/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, WriteBlanksExercise} from "@/interfaces/exam";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import FillBlanks from "./FillBlanks";
|
||||||
|
import MultipleChoice from "./MultipleChoice";
|
||||||
|
import WriteBlanks from "./WriteBlanks";
|
||||||
|
|
||||||
|
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false});
|
||||||
|
|
||||||
|
export interface CommonProps {
|
||||||
|
onNext: () => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void) => {
|
||||||
|
switch (exercise.type) {
|
||||||
|
case "fillBlanks":
|
||||||
|
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
case "matchSentences":
|
||||||
|
return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
case "multipleChoice":
|
||||||
|
return <MultipleChoice {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
case "writeBlanks":
|
||||||
|
return <WriteBlanks {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"audio": {
|
"audio": {
|
||||||
"title": "",
|
"source": "https://www.sndup.net/hqvf/d",
|
||||||
"source": "",
|
|
||||||
"transcript": "",
|
|
||||||
"repeatableTimes": 3
|
"repeatableTimes": 3
|
||||||
},
|
},
|
||||||
"module": "listening",
|
"module": "listening",
|
||||||
"minTimer": 5,
|
"minTimer": 5,
|
||||||
|
"id": "1c70c6a2-8d4c-4dfc-849c-f89786fa38dd",
|
||||||
"exercises": [
|
"exercises": [
|
||||||
{
|
{
|
||||||
"type": "multipleChoice",
|
"type": "multipleChoice",
|
||||||
"prompt": "Select the appropriate option",
|
"prompt": "Select the appropriate option",
|
||||||
|
"id": "57f21739-146d-4d1f-aed9-274f351ca27f",
|
||||||
"questions": [
|
"questions": [
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"type": "writeBlanks",
|
"type": "writeBlanks",
|
||||||
"prompt": "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided.",
|
"prompt": "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided.",
|
||||||
"maxWords": 3,
|
"maxWords": 3,
|
||||||
|
"id": "81215364-b18e-4717-9a33-a6dfc1816960",
|
||||||
"text": "The Government plans to give ${{14}} to assist the farmers. This money was to be spent on improving Sydney’s {{15}} but has now been re-allocated. Australia has experienced its worst drought in over fifty years. Farmers say that the money will not help them because it is {{16}}.",
|
"text": "The Government plans to give ${{14}} to assist the farmers. This money was to be spent on improving Sydney’s {{15}} but has now been re-allocated. Australia has experienced its worst drought in over fifty years. Farmers say that the money will not help them because it is {{16}}.",
|
||||||
"solutions": [
|
"solutions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
},
|
},
|
||||||
"module": "reading",
|
"module": "reading",
|
||||||
"minTimer": 5,
|
"minTimer": 5,
|
||||||
|
"id": "ffb738a5-265c-4daa-85c4-3681d3f8b48b",
|
||||||
"exercises": [
|
"exercises": [
|
||||||
{
|
{
|
||||||
"type": "fillBlanks",
|
"type": "fillBlanks",
|
||||||
"prompt": "Complete the summary below. Click a blank to select the corresponding word for it.\nThere are more words than spaces so you will not use them all. You may use any of the words more than once.",
|
"prompt": "Complete the summary below. Click a blank to select the corresponding word for it.\nThere are more words than spaces so you will not use them all. You may use any of the words more than once.",
|
||||||
"allowRepetition": true,
|
"allowRepetition": true,
|
||||||
|
"id": "acf930d1-3615-4b7d-b057-9f6e60b30f74",
|
||||||
"solutions": [
|
"solutions": [
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
@@ -71,6 +73,7 @@
|
|||||||
{
|
{
|
||||||
"type": "matchSentences",
|
"type": "matchSentences",
|
||||||
"prompt": "Look at the following notes that have been made about the matches described in Reading Passage 1. Decide which type of match (A-H) corresponds with each description and write your answers in boxes 9 15 on your answer sheet.",
|
"prompt": "Look at the following notes that have been made about the matches described in Reading Passage 1. Decide which type of match (A-H) corresponds with each description and write your answers in boxes 9 15 on your answer sheet.",
|
||||||
|
"id": "404437ff-2bc0-44b5-9e32-40ef4eedb023",
|
||||||
"sentences": [
|
"sentences": [
|
||||||
{
|
{
|
||||||
"id": "9",
|
"id": "9",
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
"limit": 5
|
"limit": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"id": "908286eb-2c5f-4d43-8806-0e15379143dd",
|
||||||
|
"exercises": [],
|
||||||
"minTimer": 5
|
"minTimer": 5
|
||||||
}
|
}
|
||||||
0
src/exams/Finish.tsx
Normal file
0
src/exams/Finish.tsx
Normal file
@@ -1,20 +1,23 @@
|
|||||||
import {ListeningExam} from "@/interfaces/exam";
|
import {ListeningExam, UserSolution} from "@/interfaces/exam";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {mdiArrowRight} from "@mdi/js";
|
import {mdiArrowRight} from "@mdi/js";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
import {renderExercise} from "@/components/Exercises";
|
import {renderExercise} from "@/components/Exercises";
|
||||||
|
import {renderSolution} from "@/components/Solutions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ListeningExam;
|
exam: ListeningExam;
|
||||||
onFinish: () => void;
|
showSolutions?: boolean;
|
||||||
|
onFinish: (userSolutions: UserSolution[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Listening({exam, onFinish}: Props) {
|
export default function Listening({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
const [timesListened, setTimesListened] = useState(0);
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
const [timer, setTimer] = useState<number>();
|
const [timer, setTimer] = useState<number>();
|
||||||
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimer(exam.minTimer * 60);
|
setTimer(exam.minTimer * 60);
|
||||||
@@ -25,13 +28,21 @@ export default function Listening({exam, onFinish}: Props) {
|
|||||||
};
|
};
|
||||||
}, [exam.minTimer]);
|
}, [exam.minTimer]);
|
||||||
|
|
||||||
const nextExercise = () => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
|
if (solution) {
|
||||||
|
setUserSolutions((prev) => [...prev.filter((x) => x.id !== solution.id), solution]);
|
||||||
|
}
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||||
setExerciseIndex((prev) => prev + 1);
|
setExerciseIndex((prev) => prev + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinish();
|
if (solution) {
|
||||||
|
onFinish([...userSolutions.filter((x) => x.id !== solution.id), solution].map((x) => ({...x, module: "listening"})));
|
||||||
|
} else {
|
||||||
|
onFinish(userSolutions.map((x) => ({...x, module: "listening"})));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const previousExercise = () => {
|
const previousExercise = () => {
|
||||||
@@ -53,13 +64,11 @@ export default function Listening({exam, onFinish}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="bg-gray-300 rounded-xl p-4 flex flex-col gap-4 items-center w-full overflow-auto">
|
<div className="bg-gray-300 rounded-xl p-4 flex flex-col gap-4 items-center w-full overflow-auto">
|
||||||
<span className="text-xl font-semibold">{exam.audio.title}</span>
|
|
||||||
{exam.audio.repeatableTimes > 0 && (
|
{exam.audio.repeatableTimes > 0 && (
|
||||||
<>{exam.audio.repeatableTimes <= timesListened && <span>You are no longer allowed to listen to the audio again.</span>}</>
|
<>{exam.audio.repeatableTimes <= timesListened && <span>You are no longer allowed to listen to the audio again.</span>}</>
|
||||||
)}
|
)}
|
||||||
<audio preload="auto" controls autoPlay onPlay={() => setTimesListened((prev) => prev + 1)} onPause={() => alert("PAUSE")}>
|
<audio preload="auto" controls autoPlay onPlay={() => setTimesListened((prev) => prev + 1)}>
|
||||||
<source src="https://www.sndup.net/hqvf/d" type="audio/mpeg" />
|
<source src={exam.audio.source} type="audio/mpeg" />
|
||||||
<source src="https://www.sndup.net/hqvf/m" type="audio/mpeg" />
|
|
||||||
Your browser does not support the audio element
|
Your browser does not support the audio element
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,9 +88,14 @@ export default function Listening({exam, onFinish}: Props) {
|
|||||||
{renderAudioPlayer()}
|
{renderAudioPlayer()}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
|
!showSolutions &&
|
||||||
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
|
{exerciseIndex > -1 &&
|
||||||
|
exerciseIndex < exam.exercises.length &&
|
||||||
|
showSolutions &&
|
||||||
|
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
{exerciseIndex === -1 && (
|
{exerciseIndex === -1 && (
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={nextExercise}>
|
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={() => nextExercise()}>
|
||||||
Next
|
Next
|
||||||
<div className="absolute right-4">
|
<div className="absolute right-4">
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ReadingExam} from "@/interfaces/exam";
|
import {ReadingExam, UserSolution} from "@/interfaces/exam";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {mdiArrowRight, mdiNotebook} from "@mdi/js";
|
import {mdiArrowRight, mdiNotebook} from "@mdi/js";
|
||||||
@@ -6,10 +6,12 @@ import clsx from "clsx";
|
|||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
import {renderExercise} from "@/components/Exercises";
|
import {renderExercise} from "@/components/Exercises";
|
||||||
|
import {renderSolution} from "@/components/Solutions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ReadingExam;
|
exam: ReadingExam;
|
||||||
onFinish: () => void;
|
showSolutions?: boolean;
|
||||||
|
onFinish: (userSolutions: UserSolution[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) {
|
function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) {
|
||||||
@@ -69,10 +71,11 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Reading({exam, onFinish}: Props) {
|
export default function Reading({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
const [showTextModal, setShowTextModal] = useState(false);
|
const [showTextModal, setShowTextModal] = useState(false);
|
||||||
const [timer, setTimer] = useState<number>();
|
const [timer, setTimer] = useState<number>();
|
||||||
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimer(exam.minTimer * 60);
|
setTimer(exam.minTimer * 60);
|
||||||
@@ -83,13 +86,21 @@ export default function Reading({exam, onFinish}: Props) {
|
|||||||
};
|
};
|
||||||
}, [exam.minTimer]);
|
}, [exam.minTimer]);
|
||||||
|
|
||||||
const nextExercise = () => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
|
if (solution) {
|
||||||
|
setUserSolutions((prev) => [...prev.filter((x) => x.id !== solution.id), solution]);
|
||||||
|
}
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||||
setExerciseIndex((prev) => prev + 1);
|
setExerciseIndex((prev) => prev + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinish();
|
if (solution) {
|
||||||
|
onFinish([...userSolutions.filter((x) => x.id !== solution.id), solution].map((x) => ({...x, module: "reading"})));
|
||||||
|
} else {
|
||||||
|
onFinish(userSolutions.map((x) => ({...x, module: "reading"})));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const previousExercise = () => {
|
const previousExercise = () => {
|
||||||
@@ -132,7 +143,12 @@ export default function Reading({exam, onFinish}: Props) {
|
|||||||
{exerciseIndex === -1 && renderText()}
|
{exerciseIndex === -1 && renderText()}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
|
!showSolutions &&
|
||||||
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
|
{exerciseIndex > -1 &&
|
||||||
|
exerciseIndex < exam.exercises.length &&
|
||||||
|
showSolutions &&
|
||||||
|
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
<div className={clsx("flex gap-8", exerciseIndex > -1 ? "w-full justify-between" : "self-end")}>
|
<div className={clsx("flex gap-8", exerciseIndex > -1 ? "w-full justify-between" : "self-end")}>
|
||||||
{exerciseIndex > -1 && (
|
{exerciseIndex > -1 && (
|
||||||
<button
|
<button
|
||||||
@@ -148,7 +164,7 @@ export default function Reading({exam, onFinish}: Props) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{exerciseIndex === -1 && (
|
{exerciseIndex === -1 && (
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={nextExercise}>
|
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={() => nextExercise()}>
|
||||||
Next
|
Next
|
||||||
<div className="absolute right-4">
|
<div className="absolute right-4">
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
import {WritingExam} from "@/interfaces/exam";
|
import {UserSolution, WritingExam} from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: WritingExam;
|
exam: WritingExam;
|
||||||
|
showSolutions?: boolean;
|
||||||
|
onFinish: (userSolutions: UserSolution[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Writing({exam}: Props) {
|
export default function Writing({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [inputText, setInputText] = useState("");
|
const [inputText, setInputText] = useState("");
|
||||||
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
||||||
const [timer, setTimer] = useState<number>();
|
const [timer, setTimer] = useState<number>();
|
||||||
@@ -68,13 +70,19 @@ export default function Writing({exam}: Props) {
|
|||||||
<div className="w-1/2 flex justify-end">
|
<div className="w-1/2 flex justify-end">
|
||||||
{!isSubmitEnabled && (
|
{!isSubmitEnabled && (
|
||||||
<div className="tooltip" data-tip={`You have not yet reached your minimum word count of ${exam.text.wordCounter.limit} words!`}>
|
<div className="tooltip" data-tip={`You have not yet reached your minimum word count of ${exam.text.wordCounter.limit} words!`}>
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} disabled={!isSubmitEnabled}>
|
<button
|
||||||
|
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||||
|
disabled={!isSubmitEnabled}
|
||||||
|
onClick={() => onFinish([{id: exam.id, solutions: [inputText.trim()], score: {correct: 0, total: 1}}])}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isSubmitEnabled && (
|
{isSubmitEnabled && (
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} disabled={!isSubmitEnabled}>
|
<button
|
||||||
|
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||||
|
disabled={!isSubmitEnabled}
|
||||||
|
onClick={() => onFinish([{id: exam.id, solutions: [inputText.trim()], score: {correct: 0, total: 1}}])}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {Module} from ".";
|
||||||
|
|
||||||
export type Exam = ReadingExam | ListeningExam | WritingExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam;
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
@@ -5,6 +7,7 @@ export interface ReadingExam {
|
|||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
id: string;
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
module: "reading";
|
module: "reading";
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
@@ -12,24 +15,35 @@ export interface ReadingExam {
|
|||||||
|
|
||||||
export interface ListeningExam {
|
export interface ListeningExam {
|
||||||
audio: {
|
audio: {
|
||||||
title: string;
|
|
||||||
source: string;
|
source: string;
|
||||||
transcript: string;
|
|
||||||
repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited
|
repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited
|
||||||
};
|
};
|
||||||
|
id: string;
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
module: "listening";
|
module: "listening";
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserSolution {
|
||||||
|
solutions: any[];
|
||||||
|
module?: Module;
|
||||||
|
score: {
|
||||||
|
correct: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WritingExam {
|
export interface WritingExam {
|
||||||
module: "writing";
|
module: "writing";
|
||||||
|
id: string;
|
||||||
text: {
|
text: {
|
||||||
info: string; //* The information about the task, like the amount of time they should spend on it
|
info: string; //* The information about the task, like the amount of time they should spend on it
|
||||||
prompt: string; //* The context given to the user containing what they should write about
|
prompt: string; //* The context given to the user containing what they should write about
|
||||||
wordCounter: WordCounter; //* The minimum or maximum amount of words that should be written
|
wordCounter: WordCounter; //* The minimum or maximum amount of words that should be written
|
||||||
};
|
};
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
|
exercises: Exercise[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WordCounter {
|
interface WordCounter {
|
||||||
@@ -42,6 +56,7 @@ export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleCho
|
|||||||
export interface FillBlanksExercise {
|
export interface FillBlanksExercise {
|
||||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||||
type: "fillBlanks";
|
type: "fillBlanks";
|
||||||
|
id: string;
|
||||||
words: string[]; // *EXAMPLE: ["preserve", "unaware"]
|
words: string[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||||
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||||
allowRepetition: boolean;
|
allowRepetition: boolean;
|
||||||
@@ -49,12 +64,17 @@ export interface FillBlanksExercise {
|
|||||||
id: string; // *EXAMPLE: "1"
|
id: string; // *EXAMPLE: "1"
|
||||||
solution: string; // *EXAMPLE: "preserve"
|
solution: string; // *EXAMPLE: "preserve"
|
||||||
}[];
|
}[];
|
||||||
|
userSolutions: {
|
||||||
|
id: string; // *EXAMPLE: "1"
|
||||||
|
solution: string; // *EXAMPLE: "preserve"
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WriteBlanksExercise {
|
export interface WriteBlanksExercise {
|
||||||
prompt: string; // *EXAMPLE: "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided."
|
prompt: string; // *EXAMPLE: "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided."
|
||||||
maxWords: number; // *EXAMPLE: 3 - The maximum amount of words allowed per blank, 0 for unlimited
|
maxWords: number; // *EXAMPLE: 3 - The maximum amount of words allowed per blank, 0 for unlimited
|
||||||
type: "writeBlanks";
|
type: "writeBlanks";
|
||||||
|
id: string;
|
||||||
text: string; // *EXAMPLE: "The Government plans to give ${{14}}"
|
text: string; // *EXAMPLE: "The Government plans to give ${{14}}"
|
||||||
solutions: {
|
solutions: {
|
||||||
id: string; // *EXAMPLE: "14"
|
id: string; // *EXAMPLE: "14"
|
||||||
@@ -68,6 +88,7 @@ export interface WriteBlanksExercise {
|
|||||||
|
|
||||||
export interface MatchSentencesExercise {
|
export interface MatchSentencesExercise {
|
||||||
type: "matchSentences";
|
type: "matchSentences";
|
||||||
|
id: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: {question: string; option: string}[];
|
||||||
sentences: {
|
sentences: {
|
||||||
@@ -85,6 +106,7 @@ export interface MatchSentencesExercise {
|
|||||||
|
|
||||||
export interface MultipleChoiceExercise {
|
export interface MultipleChoiceExercise {
|
||||||
type: "multipleChoice";
|
type: "multipleChoice";
|
||||||
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: MultipleChoiceQuestion[];
|
questions: MultipleChoiceQuestion[];
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: {question: string; option: string}[];
|
||||||
|
|||||||
@@ -12,20 +12,25 @@ import JSON_WRITING from "@/demo/writing.json";
|
|||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
import Reading from "@/exams/Reading";
|
||||||
import {Exam, ListeningExam, ReadingExam, WritingExam} from "@/interfaces/exam";
|
import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||||
import Listening from "@/exams/Listening";
|
import Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const [moduleIndex, setModuleIndex] = useState(0);
|
const [moduleIndex, setModuleIndex] = useState(0);
|
||||||
const [exam, setExam] = useState<Exam>();
|
const [exam, setExam] = useState<Exam>();
|
||||||
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
||||||
|
const [showSolutions, setShowSolutions] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
||||||
setExam(getExam(selectedModules[moduleIndex]));
|
const nextExam = getExam(selectedModules[moduleIndex]);
|
||||||
|
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedModules, moduleIndex]);
|
}, [selectedModules, moduleIndex]);
|
||||||
|
|
||||||
const getExam = (module: Module): Exam | undefined => {
|
const getExam = (module: Module): Exam | undefined => {
|
||||||
@@ -41,25 +46,59 @@ export default function Home() {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||||
|
const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.id)?.solutions}));
|
||||||
|
|
||||||
|
return Object.assign(exam, exercises);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = (solutions: UserSolution[]) => {
|
||||||
|
const solutionIds = solutions.map((x) => x.id);
|
||||||
|
console.log({solutions});
|
||||||
|
|
||||||
|
setUserSolutions((prev) => [...prev.filter((x) => !solutionIds.includes(x.id)), ...solutions]);
|
||||||
|
setModuleIndex((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
const renderScreen = () => {
|
const renderScreen = () => {
|
||||||
if (selectedModules.length === 0) {
|
if (selectedModules.length === 0) {
|
||||||
return <Selection user={JSON_USER} onStart={setSelectedModules} />;
|
return <Selection user={JSON_USER} onStart={setSelectedModules} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moduleIndex >= selectedModules.length) {
|
if (moduleIndex >= selectedModules.length) {
|
||||||
return <>Finished!</>;
|
return (
|
||||||
|
<>
|
||||||
|
Finished!{" "}
|
||||||
|
<button
|
||||||
|
className="btn btn-wide"
|
||||||
|
onClick={() => {
|
||||||
|
setShowSolutions(true);
|
||||||
|
setModuleIndex(0);
|
||||||
|
}}>
|
||||||
|
View Solutions
|
||||||
|
</button>
|
||||||
|
<Link href="/">
|
||||||
|
<button className="btn btn-wide">Go Home</button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exam && exam.module === "reading") {
|
if (exam && exam.module === "reading") {
|
||||||
return <Reading exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
return <Reading exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exam && exam.module === "listening") {
|
if (exam && exam.module === "listening") {
|
||||||
return <Listening exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
return <Listening exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam && exam.module === "writing" && showSolutions) {
|
||||||
|
setModuleIndex((prev) => prev + 1);
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exam && exam.module === "writing") {
|
if (exam && exam.module === "writing") {
|
||||||
return <Writing exam={exam} />;
|
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>Loading...</>;
|
return <>Loading...</>;
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
const useExamStore = create((set) => ({
|
const useExamStore = create((set) => ({}));
|
||||||
reading: undefined,
|
|
||||||
listening: undefined,
|
|
||||||
speaking: undefined,
|
|
||||||
writing: undefined,
|
|
||||||
updateModule: (module: Module, id: string) => set(() => ({[module]: id})),
|
|
||||||
clearExam: () =>
|
|
||||||
set(() => ({
|
|
||||||
reading: undefined,
|
|
||||||
listening: undefined,
|
|
||||||
speaking: undefined,
|
|
||||||
writing: undefined,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default useExamStore;
|
export default useExamStore;
|
||||||
|
|||||||
Reference in New Issue
Block a user