diff --git a/public/assets/click.png b/public/assets/click.png new file mode 100644 index 0000000..be0a3db Binary files /dev/null and b/public/assets/click.png differ diff --git a/src/components/Ending.tsx b/src/components/Ending.tsx new file mode 100644 index 0000000..6e115f8 --- /dev/null +++ b/src/components/Ending.tsx @@ -0,0 +1,25 @@ +import { FC, useEffect } from "react"; +import { Box, Divider, Typography } from "@mui/material"; + +export const Ending: FC = () => { + useEffect(() => { + sessionStorage.clear(); + }, []); + + return ( + <> + + + Congratulations + + + + You have completed the test. Thank you for participating. + + + You may now close the tab. + + + + ); +}; diff --git a/src/components/GeneralDirection.tsx b/src/components/GeneralDirection.tsx index 1ed20c8..2e4f7a3 100644 --- a/src/components/GeneralDirection.tsx +++ b/src/components/GeneralDirection.tsx @@ -1,20 +1,40 @@ import { FC, useContext } from "react"; -import { Button } from "@mui/material"; +import { Box, Button, Divider, Typography } from "@mui/material"; import { GeneralContext, Stage } from "../contexts/general.context"; export const GeneralDirection: FC = () => { const cxt = useContext(GeneralContext); const continueButtonHandler = () => { - cxt?.setStage(Stage.SOUND_CHECK); + cxt?.setStage(Stage.TRANSITION); }; return ( <> -

General Direction Placeholder

- + + + Directions + + + + Please do NOT use paper! Solve all questions in your mind. Click "Begin Test" to proceed. + + + + + ); }; diff --git a/src/components/SoundCheck.tsx b/src/components/SoundCheck.tsx index 423fafe..5cecb49 100644 --- a/src/components/SoundCheck.tsx +++ b/src/components/SoundCheck.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, IconButton, Typography } from "@mui/material"; +import { Box, Divider, Grid, IconButton, Typography } from "@mui/material"; import { FC, useContext, useEffect, useState } from "react"; import { soundCheckConfig as uiConfig } from "../config/ui.config"; import { GeneralContext, Stage } from "../contexts/general.context"; @@ -89,7 +89,7 @@ export const SoundCheck: FC = () => { if (index === cxt?.soundCheckNumber) { setTimeout(() => { - cxt?.setStage(Stage.TRANSITION); + cxt?.setStage(Stage.GENERAL_DIRECTION); }, 2000); } else { setTimeout(() => { @@ -126,18 +126,19 @@ export const SoundCheck: FC = () => { }; return ( - + Sound Check - + + If you can hear this message, click the announced number. - + Otherwise, please increase your speaks volume. - + {Array.from({ length: 3 }).map((_, rowIndex) => ( {Array.from({ length: 3 }).map((_, colIndex) => { diff --git a/src/components/Transition.tsx b/src/components/Transition.tsx index 5662765..1406eb1 100644 --- a/src/components/Transition.tsx +++ b/src/components/Transition.tsx @@ -1,8 +1,9 @@ -import { FC, useContext } from "react"; +import { FC, useContext, useState } from "react"; import { GeneralContext } from "../contexts/general.context"; import { generalConfig as testConfig } from "../config/test.config"; import { ProgressTracker } from "./ProgressTracker"; -import { Button } from "@mui/material"; +import { Box, Button } from "@mui/material"; +import { InstructionContainer, instructionComponents } from "./instructions/InstructionContainer"; interface TransitionProps { handleTransition: () => void; @@ -11,12 +12,42 @@ interface TransitionProps { export const Transition: FC = ({ handleTransition }) => { const cxt = useContext(GeneralContext); + const [displayInstructions, setDisplayInstructions] = useState(false); + return ( <> - phase === cxt!.testPhase)} /> - + {!displayInstructions && ( + + phase === cxt!.testPhase)} /> + + )} + {displayInstructions && } + + {instructionComponents[cxt!.testPhase]!.length <= 0 ? ( + + ) : ( + + )} + ); }; diff --git a/src/components/instructions/ChoiceReactionTime.tsx b/src/components/instructions/ChoiceReactionTime.tsx new file mode 100644 index 0000000..51936b7 --- /dev/null +++ b/src/components/instructions/ChoiceReactionTime.tsx @@ -0,0 +1,72 @@ +import { Box, Typography } from "@mui/material"; +import { FC } from "react"; +import { choiceReactionTimeConfig as uiConfig } from "../../config/ui.config"; +import { generalConfig as testConfig } from "../../config/test.config"; + +const color0 = uiConfig.choiceColor.color0; +const color1 = uiConfig.choiceColor.color1; + +interface CRTVisualProps { + symbols: string[]; + colors: string[]; +} + +const CRTVisual: FC = ({ symbols, colors }) => ( + + {symbols.map((symbol, index) => ( + + + {symbol} + + + ))} + +); + +export const crtInstructions: JSX.Element[] = [ + <> + ", "<", ">"]} colors={[color0, color1, color1]} /> + + Look at the 3 colored squares. + + + Two squares are the same color, one is different (ODD-COLOR). + + , + <> + ", "<", ">"]} colors={[color0, color1, color1]} /> + + Now look at the ARROW inside the ODD-COLOR. + + + When the odd-color arrow points right, tap the RIGHT-ARROW button at the{" "} + bottom of the screen with your right hand. + + , + <> + ", "<", "<"]} colors={[color0, color0, color1]} /> + + When the odd-color arrow points left, tap the LEFT-ARROW button at the{" "} + bottom of the screen with your left hand. + + , + <> + ", "<", "<"]} colors={[color0, color0, color1]} /> + + You will see {Object.keys(testConfig.choiceReactionTimeAns).length} sets of ARROWs. Please response as fast as you + can. + + , +]; diff --git a/src/components/instructions/DigitSymbolMatching.tsx b/src/components/instructions/DigitSymbolMatching.tsx new file mode 100644 index 0000000..274721e --- /dev/null +++ b/src/components/instructions/DigitSymbolMatching.tsx @@ -0,0 +1,64 @@ +import { Divider, Grid, Typography } from "@mui/material"; +import { FC } from "react"; +import { Cell } from "../tests/DigitSymbolMatchingMain"; +import { digitSymbolConfig as testConfig } from "../../config/test.config"; +import { generalConfig } from "../../config/test.config"; +import { digitSymbolConfig as uiConfig } from "../../config/ui.config"; + +const DSMVisual: FC = () => ( + + {testConfig.symbolPairs.map((symbol, index) => ( + + + + + + {symbol.num} + + + + ))} + +); + +export const dsmInstructions: JSX.Element[] = [ + <> + + + Each symbol has a number. + + , + <> + + + + When a symbol appears at the top, press its number on the number pad at the{" "} + bottom of the screen (here it is 1). + + , + <> + + You will see {Object.keys(generalConfig.digitSymbolAns).length} questions in this test. + + + Your score will be how many correct responeses you make, so try to be accurate and{" "} + quick! + + , +]; diff --git a/src/components/instructions/InstructionContainer.tsx b/src/components/instructions/InstructionContainer.tsx new file mode 100644 index 0000000..4b164eb --- /dev/null +++ b/src/components/instructions/InstructionContainer.tsx @@ -0,0 +1,83 @@ +import { FC, useState } from "react"; +import { TestPhase } from "../../contexts/general.context"; +import { Box, Button, Divider, Typography } from "@mui/material"; +import { vpmInstructions, vprInstructions } from "./VisualPairs"; +import { crtInstructions } from "./ChoiceReactionTime"; +import { dsmInstructions } from "./DigitSymbolMatching"; +import { smInstructions } from "./SpatialMemory"; +import { mrdInstructions, mriInstructions } from "./MemoryRecall"; + +const titleMapping: { [key in TestPhase]?: string } = { + [TestPhase.MEMORY_RECALL_IMMEDIATE]: "Memory - Immediate Recall", + [TestPhase.VISUAL_PAIRS_MEMORIZE]: "Visual Paired Associates - Learn", + [TestPhase.CHOICE_REACTION_TIME]: "Choice Reaction Time", + [TestPhase.VISUAL_PAIRS_RECALL]: "Visual Paired Associates - Test", + [TestPhase.DIGIT_SYMBOL_MATCHING]: "Digit Symbol Matching", + [TestPhase.SPATIAL_MEMORY]: "Spatial Memory", + [TestPhase.MEMORY_RECALL_DELAYED]: "Memory - Delayed Recall", +}; + +export const instructionComponents: { [key in TestPhase]?: JSX.Element[] } = { + [TestPhase.MEMORY_RECALL_IMMEDIATE]: mriInstructions, + [TestPhase.VISUAL_PAIRS_MEMORIZE]: vpmInstructions, + [TestPhase.CHOICE_REACTION_TIME]: crtInstructions, + [TestPhase.VISUAL_PAIRS_RECALL]: vprInstructions, + [TestPhase.DIGIT_SYMBOL_MATCHING]: dsmInstructions, + [TestPhase.SPATIAL_MEMORY]: smInstructions, + [TestPhase.MEMORY_RECALL_DELAYED]: mrdInstructions, +}; + +interface InstructionContainerProps { + phase: TestPhase; + handleTransition: () => void; +} + +export const InstructionContainer: FC = ({ phase, handleTransition }) => { + const [instructionIdx, setInstructionIdx] = useState(0); + + return ( + <> + + + {titleMapping[phase]} + + + {instructionComponents[phase]![instructionIdx]} + + + {instructionIdx + 1 < instructionComponents[phase]!.length ? ( + + ) : ( + + )} + + + ); +}; diff --git a/src/components/instructions/MemoryRecall.tsx b/src/components/instructions/MemoryRecall.tsx new file mode 100644 index 0000000..b7933dd --- /dev/null +++ b/src/components/instructions/MemoryRecall.tsx @@ -0,0 +1,23 @@ +import { Typography } from "@mui/material"; + +export const mriInstructions: JSX.Element[] = [ + <> + + You will need to play the audio and remember the five animals you hear. You need to memorize them + until the end of the test. + + , + <> + + For this section, you will be asked to select the animals you heard from the list after you hear the audio. + + , +]; + +export const mrdInstructions: JSX.Element[] = [ + <> + + You have heard the names of five animals at the beginning of the test. Select those five animals. + + , +]; diff --git a/src/components/instructions/SpatialMemory.tsx b/src/components/instructions/SpatialMemory.tsx new file mode 100644 index 0000000..5c6367c --- /dev/null +++ b/src/components/instructions/SpatialMemory.tsx @@ -0,0 +1,71 @@ +import { Grid, Typography } from "@mui/material"; +import { FC } from "react"; +import { Cell } from "../tests/SpatialMemoryMain"; +import { spatialMemoryConfig as testConfig } from "../../config/test.config"; + +const exampleMatrix0 = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +]; + +const exampleMatrix1 = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 1], +]; + +interface SMVisualProps { + matrix: number[][]; + clickPosX?: number; + clickPosY?: number; +} + +const SMVisual: FC = ({ matrix, clickPosX, clickPosY }) => ( + + {matrix.map((row: any[], rowIndex: number) => ( + + {row.map((cell: boolean, colIndex: number) => ( + + + {colIndex === clickPosX && rowIndex === clickPosY && ( + + )} + + ))} + + ))} + +); + +export const smInstructions: JSX.Element[] = [ + <> + + + Memorize the pattern on the grid + + , + <> + + + Fill in the blank squares to recreate the pattern. + + , +]; diff --git a/src/components/instructions/VisualPairs.tsx b/src/components/instructions/VisualPairs.tsx new file mode 100644 index 0000000..41e5ff5 --- /dev/null +++ b/src/components/instructions/VisualPairs.tsx @@ -0,0 +1,68 @@ +import { Box, Grid, Typography } from "@mui/material"; +import { generalConfig as testConfig } from "../../config/test.config"; +import { FC } from "react"; + +const VPVisual0: FC = () => ( + + + + +); + +const VPVisual1: FC = () => ( + + + + + + + + + + + {[3, 4].map((option, idx) => ( + + + + ))} + + + {[5, 6].map((option, idx) => ( + + + + ))} + + +); + +export const vpmInstructions: JSX.Element[] = [ + <> + + + You will see {Object.keys(testConfig.visualPairsAns).length} image pairs, like above. Learn which images go + together. Later you will be tested on that. + + , +]; + +export const vprInstructions: JSX.Element[] = [ + <> + + + Let's test your memory for the images you learned a few minutes ago. + + , + <> + + + You will need to select the image that goes together with the image with red outline at the{" "} + top left corner. + + , + <> + + You will be asked to recall all {Object.keys(testConfig.visualPairsAns).length} pairs of images. Let's start! + + , +]; diff --git a/src/components/tests/DigitSymbolMatchingMain.tsx b/src/components/tests/DigitSymbolMatchingMain.tsx index 5ab1426..aca7c50 100644 --- a/src/components/tests/DigitSymbolMatchingMain.tsx +++ b/src/components/tests/DigitSymbolMatchingMain.tsx @@ -8,7 +8,7 @@ import { TestContext } from "../../contexts/test.context"; import { getNextTestPhase } from "../../utils/general.utils"; import { DigitSymbolMatchingResult } from "../../contexts/types/result.type"; -const Cell = styled(Box, { +export const Cell = styled(Box, { shouldForwardProp: (prop) => prop !== "leftBox" && prop !== "rightBox" && prop !== "middleBox", })<{ leftBox?: boolean; rightBox?: boolean; middleBox?: boolean }>(({ leftBox, rightBox }) => ({ borderLeft: leftBox ? "2px solid black" : "1px solid black", diff --git a/src/components/tests/MemoryRecallMain.tsx b/src/components/tests/MemoryRecallMain.tsx index 41482f1..c8dadb1 100644 --- a/src/components/tests/MemoryRecallMain.tsx +++ b/src/components/tests/MemoryRecallMain.tsx @@ -154,18 +154,18 @@ export const MemoryRecallMain: FC = ({ phase, toTestPhase top: 0, left: 0, right: 0, - width: "80vw", + width: "85vw", marginX: "auto", marginY: 4, }} > {!showInstruction && phase === TestPhase.MEMORY_RECALL_IMMEDIATE && ( - + Please memorize these five animals until the end of the test. )} {showInstruction && ( - + Select the five animals you have heard. )} @@ -176,7 +176,7 @@ export const MemoryRecallMain: FC = ({ phase, toTestPhase )} {showOptions && ( - + {Array.from({ length: 8 }).map((_, rowIndex) => ( {Array.from({ length: 2 }).map((_, colIndex) => { diff --git a/src/components/tests/SpatialMemoryMain.tsx b/src/components/tests/SpatialMemoryMain.tsx index 81ac8cf..33a7aaf 100644 --- a/src/components/tests/SpatialMemoryMain.tsx +++ b/src/components/tests/SpatialMemoryMain.tsx @@ -6,20 +6,25 @@ import { TestContext } from "../../contexts/test.context"; import { TestPhase } from "../../contexts/general.context"; import { SpatialMemoryResult } from "../../contexts/types/result.type"; -const Cell = styled(Box, { +export const Cell = styled(Box, { shouldForwardProp: (prop) => prop !== "topBox" && prop !== "bottomBox" && prop !== "leftBox" && prop !== "rightBox", -})<{ topBox?: boolean; bottomBox?: boolean; leftBox?: boolean; rightBox?: boolean }>( - ({ theme, topBox, bottomBox, leftBox, rightBox }) => ({ - width: uiConfig.cellSize, - height: uiConfig.cellSize, - backgroundColor: theme.palette.background.paper, - borderTop: topBox ? "3px solid black" : "1px solid black", - borderBottom: bottomBox ? "3px solid black" : "1px solid black", - borderLeft: leftBox ? "3px solid black" : "1px solid black", - borderRight: rightBox ? "3px solid black" : "1px solid black", - transition: "background-color 0.2s ease", - }) -); +})<{ + topBox?: boolean; + bottomBox?: boolean; + leftBox?: boolean; + rightBox?: boolean; + cellwidth?: string; + cellheight?: string; +}>(({ theme, topBox, bottomBox, leftBox, rightBox, cellwidth, cellheight }) => ({ + width: cellwidth ?? uiConfig.cellSize, + height: cellheight ?? uiConfig.cellSize, + backgroundColor: theme.palette.background.paper, + borderTop: topBox ? "3px solid black" : "1px solid black", + borderBottom: bottomBox ? "3px solid black" : "1px solid black", + borderLeft: leftBox ? "3px solid black" : "1px solid black", + borderRight: rightBox ? "3px solid black" : "1px solid black", + transition: "background-color 0.2s ease", +})); interface SpatialMemoryMainProps { toTestPhase: (testPhase: TestPhase) => void; @@ -130,7 +135,13 @@ export const SpatialMemoryMain: FC = ({ toTestPhase }) = p: 1, }} > - diff --git a/src/config/test.config.ts b/src/config/test.config.ts index 3b73b1f..ae8d1ad 100644 --- a/src/config/test.config.ts +++ b/src/config/test.config.ts @@ -23,7 +23,7 @@ export const generalConfig = { digitSymbolAns: [3, 8, 4, 0, 7, 0, 3, 5, 8, 2], choiceReactionTimeAns: [2, 2, 1, 1, 0, 2, 1, 0, 1, 0] as (0 | 1 | 2)[], visualPairsAns: { - example: [3, 1], + bar: [3, 1], lobby: [1, 2], underwater: [8, 5], temple: [4, 6], diff --git a/src/config/ui.config.ts b/src/config/ui.config.ts index 644357a..b2d6727 100644 --- a/src/config/ui.config.ts +++ b/src/config/ui.config.ts @@ -51,7 +51,7 @@ export const spatialMemoryConfig = { * @property textColor - Text color configuration */ export const memoryRecallConfig = { - buttonWidth: "40vw", + buttonWidth: "42vw", buttonHeight: "8vh", fontSize: 25, buttonColor: { diff --git a/src/contexts/general.context.tsx b/src/contexts/general.context.tsx index 328c283..125ef11 100644 --- a/src/contexts/general.context.tsx +++ b/src/contexts/general.context.tsx @@ -2,10 +2,11 @@ import { createContext, Dispatch, FC, ReactNode, SetStateAction, useState } from export enum Stage { NULL = 0, - GENERAL_DIRECTION = 1, - SOUND_CHECK = 2, + SOUND_CHECK = 1, + GENERAL_DIRECTION = 2, TRANSITION = 3, TEST = 4, + ENDING = 5, } export enum TestPhase { diff --git a/src/index.css b/src/index.css index f5f1d13..071d25d 100644 --- a/src/index.css +++ b/src/index.css @@ -52,7 +52,7 @@ button:hover { } button:focus, button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + outline: none; } @media (prefers-color-scheme: light) { diff --git a/src/pages/TestPage.tsx b/src/pages/TestPage.tsx index 7007bfe..8eba21d 100644 --- a/src/pages/TestPage.tsx +++ b/src/pages/TestPage.tsx @@ -11,6 +11,7 @@ import { SoundCheck } from "../components/SoundCheck"; import { GeneralDirection } from "../components/GeneralDirection"; import { useNavigate, useParams } from "react-router-dom"; import { generalConfig } from "../config/test.config"; +import { Ending } from "../components/Ending"; export const TestPage: FC = () => { const { studyId } = useParams<{ studyId: string }>(); @@ -28,10 +29,10 @@ export const TestPage: FC = () => { if (!sessionStorage.getItem("testPhase") || !sessionStorage.getItem("stage")) { sessionStorage.setItem("testPhase", String(generalConfig.testOrder[0])); - sessionStorage.setItem("stage", String(Stage.GENERAL_DIRECTION)); + sessionStorage.setItem("stage", String(Stage.SOUND_CHECK)); sessionStorage.setItem("questionNumber", "0"); cxt!.setTestPhase(generalConfig.testOrder[0]); - cxt!.setStage(Stage.GENERAL_DIRECTION); + cxt!.setStage(Stage.SOUND_CHECK); } else { cxt!.setTestPhase(Number(sessionStorage.getItem("testPhase")) as TestPhase); cxt!.setStage(Number(sessionStorage.getItem("stage")) as Stage); @@ -45,8 +46,18 @@ export const TestPage: FC = () => { sessionStorage.setItem("results", "[]"); sessionStorage.setItem("testPhase", String(target)); - sessionStorage.setItem("stage", String(Stage.TRANSITION)); sessionStorage.setItem("questionNumber", "0"); + + if (target === TestPhase.FINISHED) { + sessionStorage.setItem("stage", String(Stage.ENDING)); + cxt!.setTestPhase(target); + cxt!.setStage(Stage.ENDING); + + return; + } + + sessionStorage.setItem("stage", String(Stage.TRANSITION)); + cxt!.setTestPhase(target); cxt!.setStage(Stage.TRANSITION); }; @@ -85,10 +96,11 @@ export const TestPage: FC = () => { return ( <> - {cxt?.stage === Stage.GENERAL_DIRECTION && } {cxt?.stage === Stage.SOUND_CHECK && } + {cxt?.stage === Stage.GENERAL_DIRECTION && } {cxt?.stage === Stage.TRANSITION && } {cxt?.stage === Stage.TEST && } + {cxt?.stage === Stage.ENDING && } ); };