Updated the FillBlanks exercise and solution to the new design
This commit is contained in:
2047
.pnp.loader.mjs
generated
2047
.pnp.loader.mjs
generated
File diff suppressed because it is too large
Load Diff
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
yarnPath: .yarn/releases/yarn-1.22.1.cjs
|
||||||
@@ -54,5 +54,6 @@
|
|||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,75 +4,81 @@ import {Dialog, Transition} from "@headlessui/react";
|
|||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
interface WordsPopoutProps {
|
interface WordsDrawerProps {
|
||||||
words: {word: string; isDisabled: boolean}[];
|
words: {word: string; isDisabled: boolean}[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
blankId?: string;
|
||||||
|
previouslySelectedWord?: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onAnswer: (answer: string) => void;
|
onAnswer: (answer: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}: WordsDrawerProps) {
|
||||||
return (
|
const [selectedWord, setSelectedWord] = useState<string | undefined>(previouslySelectedWord);
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={onCancel}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0">
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
useEffect(() => setSelectedWord(previouslySelectedWord), [previouslySelectedWord]);
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<Transition.Child
|
return (
|
||||||
as={Fragment}
|
<>
|
||||||
enter="ease-out duration-300"
|
<div
|
||||||
enterFrom="opacity-0 scale-95"
|
className={clsx(
|
||||||
enterTo="opacity-100 scale-100"
|
"w-full h-full absolute top-0 left-0 bg-gradient-to-t from-mti-black to-transparent z-10",
|
||||||
leave="ease-in duration-200"
|
isOpen ? "visible opacity-10" : "invisible opacity-0",
|
||||||
leaveFrom="opacity-100 scale-100"
|
)}
|
||||||
leaveTo="opacity-0 scale-95">
|
/>
|
||||||
<Dialog.Panel className="w-fit transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all flex flex-col">
|
<div
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
className={clsx(
|
||||||
List of words
|
"absolute w-full bg-white px-7 py-8 bottom-0 left-0 shadow-2xl rounded-2xl z-20 flex flex-col gap-8 transition-opacity duration-300 ease-in-out",
|
||||||
</Dialog.Title>
|
isOpen ? "visible opacity-100" : "invisible opacity-0",
|
||||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
)}>
|
||||||
{words.map((word) => (
|
<div className="w-full flex gap-2">
|
||||||
|
<div className="rounded-full w-6 h-6 flex items-center justify-center text-white bg-mti-green-light">{blankId}</div>
|
||||||
|
<span> Choose the correct word:</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-6 gap-6">
|
||||||
|
{words.map(({word, isDisabled}) => (
|
||||||
<button
|
<button
|
||||||
key={word.word}
|
key={word}
|
||||||
onClick={() => onAnswer(word.word)}
|
onClick={() => setSelectedWord((prev) => (prev === word ? undefined : word))}
|
||||||
disabled={word.isDisabled}
|
className={clsx(
|
||||||
className={clsx("btn sm:btn-wide gap-4 relative text-white", infoButtonStyle)}>
|
"rounded-full py-3 text-center transition duration-300 ease-in-out",
|
||||||
{word.word}
|
selectedWord === word ? "text-white bg-mti-green-light" : "bg-mti-green-ultralight",
|
||||||
|
!isDisabled && "hover:text-white hover:bg-mti-green",
|
||||||
|
"disabled:cursor-not-allowed disabled:text-mti-gray-dim",
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}>
|
||||||
|
{word}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between w-full">
|
||||||
<div className="mt-4 self-end">
|
<Button color="green" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
|
||||||
<button onClick={onCancel} className={clsx("btn md:btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
Back
|
||||||
Close
|
</Button>
|
||||||
</button>
|
<Button color="green" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
|
||||||
</div>
|
Confirm
|
||||||
</Dialog.Panel>
|
</Button>
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</>
|
||||||
</Transition>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FillBlanks({id, allowRepetition, type, prompt, solutions, text, words, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
export default function FillBlanks({id, allowRepetition, type, 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 [blankSelectedWord, setBlankSelectedWord] = useState<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBlankSelectedWord(currentBlankId ? userSolutions.find((x) => x.id === currentBlankId)?.solution : undefined);
|
||||||
|
}, [userSolutions, currentBlankId]);
|
||||||
|
|
||||||
|
useEffect(() => console.log({blankSelectedWord}), [blankSelectedWord]);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
@@ -81,15 +87,26 @@ export default function FillBlanks({id, allowRepetition, type, prompt, solutions
|
|||||||
return {total, correct};
|
return {total, correct};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(currentBlankId, userSolutions.find((x) => x.id === currentBlankId)?.solution);
|
||||||
|
}, [userSolutions, currentBlankId]);
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="text-base leading-5">
|
||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
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);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="border-2 rounded-xl px-4 text-blue-400 border-blue-400 my-2" onClick={() => setCurrentBlankId(id)}>
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-full hover:text-white hover:bg-mti-green transition duration-300 ease-in-out my-1",
|
||||||
|
!userSolution && "w-6 h-6 text-center text-mti-green-light bg-mti-green-ultralight",
|
||||||
|
currentBlankId === id && "text-white !bg-mti-green-light ",
|
||||||
|
userSolution && "px-5 py-2 text-center text-white bg-mti-green-light",
|
||||||
|
)}
|
||||||
|
onClick={() => setCurrentBlankId(id)}>
|
||||||
{userSolution ? userSolution.solution : id}
|
{userSolution ? userSolution.solution : id}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -100,9 +117,11 @@ export default function FillBlanks({id, allowRepetition, type, prompt, solutions
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4 mt-4 h-full mb-20">
|
||||||
<WordsPopout
|
<WordsDrawer
|
||||||
|
blankId={currentBlankId}
|
||||||
words={words.map((word) => ({word, isDisabled: allowRepetition ? false : userSolutions.map((x) => x.solution).includes(word)}))}
|
words={words.map((word) => ({word, isDisabled: allowRepetition ? false : userSolutions.map((x) => x.solution).includes(word)}))}
|
||||||
|
previouslySelectedWord={blankSelectedWord}
|
||||||
isOpen={!!currentBlankId}
|
isOpen={!!currentBlankId}
|
||||||
onCancel={() => setCurrentBlankId(undefined)}
|
onCancel={() => setCurrentBlankId(undefined)}
|
||||||
onAnswer={(solution: string) => {
|
onAnswer={(solution: string) => {
|
||||||
@@ -110,7 +129,7 @@ export default function FillBlanks({id, allowRepetition, type, prompt, solutions
|
|||||||
setCurrentBlankId(undefined);
|
setCurrentBlankId(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-base md:text-lg font-medium text-center px-2 md:px-4 lg:px-48">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{line}
|
{line}
|
||||||
@@ -118,31 +137,27 @@ export default function FillBlanks({id, allowRepetition, type, prompt, solutions
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||||
{text.split("\\n").map((line, index) => (
|
{text.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<p key={index}>
|
||||||
{renderLines(line)}
|
{renderLines(line)}
|
||||||
<br />
|
<br />
|
||||||
</Fragment>
|
</p>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex flex-col-reverse items-center w-full md:justify-between md:items-start md:flex-row gap-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={onBack}>
|
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
|
||||||
<div className="absolute left-4">
|
|
||||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
|
||||||
</div>
|
|
||||||
Back
|
Back
|
||||||
</button>
|
</Button>
|
||||||
<button
|
|
||||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
<Button
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}>
|
color="green"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
<div className="absolute right-4">
|
</Button>
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export default function Layout({user, children, className}: Props) {
|
|||||||
<div className="h-full w-full flex py-4 pb-8 gap-2">
|
<div className="h-full w-full flex py-4 pb-8 gap-2">
|
||||||
<Sidebar path={window.location.pathname} />
|
<Sidebar path={window.location.pathname} />
|
||||||
<div
|
<div
|
||||||
className={clsx("w-5/6 min-h-full h-fit mr-8 bg-white shadow-md rounded-2xl p-12 pb-8 flex flex-col gap-12 relative", className)}>
|
className={clsx(
|
||||||
|
"w-5/6 min-h-full h-fit mr-8 bg-white shadow-md rounded-2xl p-12 pb-8 flex flex-col gap-12 relative overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import clsx from "clsx";
|
|||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import {Fragment} from "react";
|
import {Fragment} from "react";
|
||||||
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
export default function FillBlanksSolutions({prompt, solutions, text, userSolutions, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
export default function FillBlanksSolutions({prompt, solutions, text, userSolutions, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
@@ -18,23 +19,46 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
|||||||
|
|
||||||
if (!userSolution) {
|
if (!userSolution) {
|
||||||
return (
|
return (
|
||||||
<>
|
<button
|
||||||
<button className={clsx("border-2 rounded-xl px-4 text-gray-500 border-gray-500 my-2")}>{solution.solution}</button>
|
className={clsx(
|
||||||
</>
|
"rounded-full hover:text-white hover:bg-mti-blue transition duration-300 ease-in-out my-1",
|
||||||
|
userSolution && "px-5 py-2 text-center text-white bg-mti-blue-light",
|
||||||
|
)}>
|
||||||
|
{solution.solution}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSolution.solution === solution.solution) {
|
if (userSolution.solution === solution.solution) {
|
||||||
return <button className={clsx("border-2 rounded-xl px-4 text-green-500 border-green-500 my-2")}>{solution.solution}</button>;
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-full hover:text-white hover:bg-mti-green transition duration-300 ease-in-out my-1",
|
||||||
|
userSolution && "px-5 py-2 text-center text-white bg-mti-green-light",
|
||||||
|
)}>
|
||||||
|
{solution.solution}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSolution.solution !== solution.solution) {
|
if (userSolution.solution !== solution.solution) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button className={clsx("border-2 rounded-xl px-4 text-red-500 border-red-500 mr-1 my-2")}>
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-full hover:text-white hover:bg-mti-orange transition duration-300 ease-in-out my-1",
|
||||||
|
userSolution && "px-5 py-2 text-center text-white bg-mti-orange-light",
|
||||||
|
)}>
|
||||||
{userSolution.solution}
|
{userSolution.solution}
|
||||||
</button>
|
</button>
|
||||||
<button className={clsx("border-2 rounded-xl px-4 text-green-400 border-green-400 my-2")}>{solution.solution}</button>
|
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-full hover:text-white hover:bg-mti-green transition duration-300 ease-in-out my-1",
|
||||||
|
userSolution && "px-5 py-2 text-center text-white bg-mti-green-light",
|
||||||
|
)}>
|
||||||
|
{solution.solution}
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -45,8 +69,8 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4 mt-4 h-full mb-20">
|
||||||
<span className="text-base md:text-lg font-medium text-center px-2 md:px-4 lg:px-48">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{line}
|
{line}
|
||||||
@@ -54,29 +78,24 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||||
{text.split("\\n").map((line, index) => (
|
{text.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<p key={index}>
|
||||||
{renderLines(line)}
|
{renderLines(line)}
|
||||||
<br />
|
<br />
|
||||||
</Fragment>
|
</p>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex flex-col-reverse items-center w-full md:justify-between md:items-start md:flex-row gap-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={onBack}>
|
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
|
||||||
<div className="absolute left-4">
|
|
||||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
|
||||||
</div>
|
|
||||||
Back
|
Back
|
||||||
</button>
|
</Button>
|
||||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
|
||||||
|
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
<div className="absolute right-4">
|
</Button>
|
||||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user