177 lines
7.0 KiB
TypeScript
177 lines
7.0 KiB
TypeScript
import { Button, Group, Stack, Text, Box, Transition } from "@mantine/core";
|
|
import classes from './InputChain.module.css'
|
|
import { useEffect, useState } from "react";
|
|
|
|
interface InputChainProps {
|
|
text: string;
|
|
hidden: number;
|
|
onPassed: () => void;
|
|
onNotPassed?: () => void;
|
|
size?: string;
|
|
w?: number;
|
|
}
|
|
|
|
export function InputChainHidden(props : InputChainProps) {
|
|
const text = props.text;
|
|
if(text.trim() == ""){
|
|
return (<></>);
|
|
}
|
|
|
|
const words = text.split(" ");
|
|
const [hiddenIndices, setHiddenIndices] = useState<number[]>([]);
|
|
const [selectedWords, setSelectedWords] = useState<(string | null)[]>([]);
|
|
const [availableWords, setAvailableWords] = useState<string[]>([]);
|
|
const [wrongIndices, setWrongIndices] = useState<number[]>([]);
|
|
const [correctIndices, setCorrectIndices] = useState<number[]>([]);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let hidden : number[] = [];
|
|
while (hidden.length < props.hidden) {
|
|
let num = Math.floor(Math.random() * words.length);
|
|
if(hidden.indexOf(num) == -1){
|
|
hidden.push(num);
|
|
}
|
|
}
|
|
hidden.sort((a, b) => a - b);
|
|
setHiddenIndices(hidden);
|
|
|
|
const hiddenWords = hidden.map(idx => words[idx]);
|
|
const shuffled = [...hiddenWords].sort(() => Math.random() - 0.5);
|
|
setAvailableWords(shuffled);
|
|
setSelectedWords(new Array(hidden.length).fill(null));
|
|
|
|
setTimeout(() => setMounted(true), 100);
|
|
}, []);
|
|
|
|
const handleWordClick = (word: string) => {
|
|
const firstEmptyIndex = selectedWords.findIndex(w => w === null);
|
|
if (firstEmptyIndex !== -1) {
|
|
const newSelected = [...selectedWords];
|
|
newSelected[firstEmptyIndex] = word;
|
|
setSelectedWords(newSelected);
|
|
setAvailableWords(availableWords.filter(w => w !== word));
|
|
checkIfPassed(newSelected);
|
|
validateWords(newSelected);
|
|
}
|
|
};
|
|
|
|
const handleRemoveWord = (index: number) => {
|
|
const word = selectedWords[index];
|
|
if (word) {
|
|
const newSelected = [...selectedWords];
|
|
newSelected[index] = null;
|
|
setSelectedWords(newSelected);
|
|
setAvailableWords([...availableWords, word]);
|
|
if(props.onNotPassed){
|
|
props.onNotPassed();
|
|
}
|
|
validateWords(newSelected);
|
|
}
|
|
};
|
|
|
|
const validateWords = (selected: (string | null)[]) => {
|
|
const wrong: number[] = [];
|
|
const correct: number[] = [];
|
|
selected.forEach((word, idx) => {
|
|
if (word !== null) {
|
|
if (word === words[hiddenIndices[idx]]) {
|
|
correct.push(idx);
|
|
} else {
|
|
wrong.push(idx);
|
|
}
|
|
}
|
|
});
|
|
setWrongIndices(wrong);
|
|
setCorrectIndices(correct);
|
|
};
|
|
|
|
const checkIfPassed = (selected: (string | null)[]) => {
|
|
const allFilled = selected.every(w => w !== null);
|
|
if (allFilled) {
|
|
const isCorrect = selected.every((word, idx) =>
|
|
word === words[hiddenIndices[idx]]
|
|
);
|
|
if (isCorrect) {
|
|
props.onPassed();
|
|
} else if(props.onNotPassed) {
|
|
props.onNotPassed();
|
|
}
|
|
} else {
|
|
if(props.onNotPassed){
|
|
props.onNotPassed();
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Stack gap="sm">
|
|
{/* Target area - where words should be placed */}
|
|
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
|
|
{(styles) => (
|
|
<Box className={classes.targetArea} style={styles}>
|
|
<Text size="sm" mb="xs" c="dimmed">
|
|
Click the words in the correct order:
|
|
</Text>
|
|
<Group gap="xs">
|
|
{words.map((word, i) => {
|
|
const hiddenIdx = hiddenIndices.indexOf(i);
|
|
const isHidden = hiddenIdx !== -1;
|
|
|
|
if (!isHidden) {
|
|
return (
|
|
<Box key={i} className={classes.staticWord}>
|
|
<Text size="sm" c="dimmed">{i + 1}.</Text>
|
|
<Text size="sm">{word}</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const isWrong = wrongIndices.includes(hiddenIdx);
|
|
const isCorrect = correctIndices.includes(hiddenIdx);
|
|
|
|
return (
|
|
<Button
|
|
key={i}
|
|
variant={selectedWords[hiddenIdx] ? "filled" : "default"}
|
|
size="sm"
|
|
className={classes.targetSlot}
|
|
color={isWrong ? "red" : isCorrect ? "green" : undefined}
|
|
onClick={() => selectedWords[hiddenIdx] && handleRemoveWord(hiddenIdx)}
|
|
>
|
|
<Text size="sm" c={selectedWords[hiddenIdx] ? 'white' : 'dimmed'} mr={4}>{i + 1}.</Text>
|
|
{selectedWords[hiddenIdx] || "_____"}
|
|
</Button>
|
|
);
|
|
})}
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
</Transition>
|
|
|
|
{/* Available words area */}
|
|
<Transition mounted={mounted} transition="slide-up" duration={500} timingFunction="ease">
|
|
{(transitionStyles) => (
|
|
<Box mih={100} className={classes.availableArea} style={transitionStyles}>
|
|
<Text size="sm" mb="xs" c="dimmed">
|
|
Available words:
|
|
</Text>
|
|
<Group gap="xs">
|
|
{availableWords.map((word, idx) => (
|
|
<Button
|
|
key={`${word}-${idx}`}
|
|
variant="light"
|
|
size="sm"
|
|
onClick={() => handleWordClick(word)}
|
|
className={classes.availableWord}
|
|
>
|
|
{word}
|
|
</Button>
|
|
))}
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
</Transition>
|
|
</Stack>
|
|
);
|
|
} |