'init'
This commit is contained in:
46
app/components/InputChainHidden/InputChain.module.css
Normal file
46
app/components/InputChainHidden/InputChain.module.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.wrapper{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.targetArea {
|
||||
padding: 16px;
|
||||
border: 2px dashed light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
border-radius: 8px;
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
.availableArea {
|
||||
padding: 16px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
border-radius: 8px;
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.staticWord {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.targetSlot {
|
||||
min-width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.targetSlot:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.availableWord {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.availableWord:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
177
app/components/InputChainHidden/InputChainHidden.tsx
Normal file
177
app/components/InputChainHidden/InputChainHidden.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user