Skip to content
Snippets Groups Projects
Commit a522aeb6 authored by Katarína Sieklová's avatar Katarína Sieklová
Browse files

feat: DragNDrop test!!!

parent 9481027c
No related branches found
No related tags found
No related merge requests found
Pipeline #
interface ExplanationPopupTypes {
explanation: string,
className: string,
}
const ExplanationPopup = ({ explanation, className }: ExplanationPopupTypes) => {
return (
<div className={className}>
<p>{explanation}</p>
</div>
);
};
export default ExplanationPopup;
......@@ -55,7 +55,6 @@ const Intro = () => {
<img
src="src/assets/backgrounds/homepage-static.svg"
alt="background picture"
className=""
/>
</div>
</div>
......
import {DragDropContext, Draggable, DraggableProvided, DroppableProvided, DropResult} from 'react-beautiful-dnd';
import {StrictModeDroppable} from "./StrictModeDroppable";
import {Dispatch, SetStateAction, useEffect, useState} from "react";
import {Texts} from "../hooks/useAnimate";
import {useRecoilValue} from "recoil";
import {languageAtom} from "../atoms/languageAtom";
import {glucoseTimeline} from "../../public/lit/texts";
interface GlucoseTimelineItem {
id: string;
text: {
EN: string;
SK: string;
};
image: string;
}
interface InsulinDragTestProps {
handleNext: () => void,
setScore: Dispatch<SetStateAction<number>>,
score: number
}
const InsulinDragNDropTest = ({ handleNext, setScore, score }: InsulinDragTestProps) => {
const shuffleArray = (array: GlucoseTimelineItem[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
const initialOrder = shuffleArray([...glucoseTimeline]);
const [timeline, setTimeline] = useState(initialOrder);
const language = useRecoilValue(languageAtom);
const [isCorrectOrder, setIsCorrectOrder] = useState(false);
const handleOnDragEnd = (result: DropResult) => {
console.log(result);
if (!result.destination) {
return;
}
const items = Array.from(timeline);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setTimeline(items);
}
useEffect(() => {
const isOrderCorrect = timeline.every((item, index) => item.id === glucoseTimeline[index].id);
setIsCorrectOrder(isOrderCorrect);
}, [timeline]);
const handleNextClick = () => {
setScore(score + 1);
handleNext();
};
return (
<>
<p className="text-lg">Can you tell the story of how our food can give us energy?</p>
<div className="w-1/3 inline-block mt-4">
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="glucoseTimeline">
{(provided: DroppableProvided) => (
<ul className={`glucoseTimeline ${isCorrectOrder ? 'correct' : ''}`} {...provided.droppableProps}
ref={provided.innerRef}>
{timeline.map(({id, text, image}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided: DraggableProvided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<div className="timeline-image">
<img src={image}
alt={`${text[language as keyof Texts]} image`}/>
</div>
<p className="text-lg">
{text[language as keyof Texts]}
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</StrictModeDroppable>
</DragDropContext>
{isCorrectOrder && <p className="mb-4 bg-green-500">CORRECT!</p>}
{isCorrectOrder && <button className="border-4 border-black font-primary bg-yellow-main"
onClick={handleNextClick}>Next</button>}
</div>
</>
)
}
export default InsulinDragNDropTest;
interface InsulinExplanationPopupTypes {
explanation: string,
}
const InsulinExplanationPopup = ({ explanation }: InsulinExplanationPopupTypes) => {
return (
<div className="fixed top-18 right-4 bg-white p-4 border border-gray-300 rounded shadow-lg max-w-xs z-10 flex">
<p>{explanation}</p>
</div>
);
};
export default InsulinExplanationPopup;
......@@ -13,6 +13,7 @@ import {languageAtom} from "../atoms/languageAtom";
import {Texts} from "../hooks/useAnimate";
import {useTranslate} from "../hooks/useTranslate";
import {useNavigate} from "react-router-dom";
import InsulinDragNDropTest from "./InsulinDragNDropTest";
const InsulinTest = () => {
const language = useRecoilValue(languageAtom);
......@@ -21,7 +22,7 @@ const InsulinTest = () => {
const finishButtonText = useTranslate(finishButton);
const correctButtonText = useTranslate(correct);
const scoreText = useTranslate(yourScore);
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [isAnswered, setIsAnswered] = useState(false);
const [score, setScore] = useState(0);
const navigate = useNavigate();
......@@ -32,7 +33,7 @@ const InsulinTest = () => {
setIsAnswered(false);
};
const handleAnswerClick = (index: any) => {
const handleAnswerClick = (index: number) => {
if (!isAnswered) {
setSelectedAnswer(index);
setIsAnswered(true);
......@@ -42,8 +43,19 @@ const InsulinTest = () => {
}
};
const isLastStep = currentStep === insulinTestQuestions.length - 1;
const isSummaryStep = currentStep === insulinTestQuestions.length;
const generateSteps = () => {
const updatedSteps = stepperLabels.map((question) => ({
label: question[language as keyof Texts],
description: "",
}));
updatedSteps.splice(updatedSteps.length - 1, 0, { label: "Glucose Timeline", description: "" });
return updatedSteps;
};
const isDraggableTestStep = currentStep === 4;
const isLastStep = currentStep === 6;
const isSummaryStep = currentStep === 5;
return (
<div className="container mx-auto py-8">
......@@ -53,11 +65,7 @@ const InsulinTest = () => {
pending: '#aaccff',
progress: '#5599ff',
}}
steps={stepperLabels.map((question) => ({
label: question[language as keyof Texts],
description: ""
}))}
steps={generateSteps()}
activeStep={currentStep}
/>
<div className="text-center">
......@@ -65,6 +73,8 @@ const InsulinTest = () => {
<p className="text-xl font-semibold py-4">
{`${scoreText} ${(score / insulinTestQuestions.length) * 100}%`}
</p>
) : isDraggableTestStep ? (
<></>
) : (
<p className="text-xl font-semibold py-4">
{language === "EN"
......@@ -81,6 +91,8 @@ const InsulinTest = () => {
>
{nextButtonText}
</button>
) : isDraggableTestStep ? (
<InsulinDragNDropTest handleNext={handleNextStep} setScore={setScore} score={score}/>
) : (
<div>
<div>
......
......@@ -44,8 +44,7 @@ const Intro = () => {
</div>
<div className="mt-12 ml-44 flex justify-center">
<div
id="animation-container"
style={{width: '600px', height: '300px', position: 'relative'}}
style={{width: '600px', maxWidth: '600px', height: '300px', position: 'relative'}}
>
<AnimateCC
animationName="intro"
......
import {useRef, useState} from 'react';
import {useEffect, useRef, useState} from 'react';
import {AnimateCC} from 'react-adobe-animate';
import Typewriter, {TypewriterClass} from "typewriter-effect";
import {insulinExplanation, nextButton, opening, skipToTest, toTest} from "../../public/lit/texts";
import {glucoseExplanation, insulinExplanation, nextButton, opening, skipToTest, toTest} from "../../public/lit/texts";
import {useAnimate} from "../hooks/useAnimate";
import {useTranslate} from "../hooks/useTranslate";
import {useNavigate} from "react-router-dom";
import InsulinExplanationPopup from "./InsulinExplanationPopUp";
import ExplanationPopup from "./ExplanationPopup";
import {useRecoilValue} from "recoil";
import {languageAtom} from "../atoms/languageAtom";
......@@ -15,23 +15,44 @@ const Opening = () => {
const typewriterRef = useRef<TypewriterClass>();
const skipButtonText = useTranslate(skipToTest);
const insulinExplanationText = useTranslate(insulinExplanation);
const glucoseExplanationText = useTranslate(glucoseExplanation);
const navigate = useNavigate();
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [isAnimPopupOpen, setIsAnimPopupOpen] = useState(false);
const language = useRecoilValue(languageAtom);
const togglePopup = () => {
setIsPopupOpen(!isPopupOpen);
};
const {getAnimationObject, handleClick, isAnimationComplete, isLast, onInit, paused} = useAnimate({
const {
getAnimationObject, handleClick, isAnimationComplete, isLast, onInit,
paused, animationObj
} = useAnimate({
typewriterRef,
link: '/insulinTest',
text: opening,
audioFiles: []
});
useEffect(() => {
getAnimationObject((prev: any) => {
if (prev === undefined || prev === null) return prev;
const glucoseInstance = prev["glucose"];
console.log("glucoseInstance:", glucoseInstance);
if (glucoseInstance) {
if (!glucoseInstance._listeners?.click) {
glucoseInstance.on("click", () => {
setIsAnimPopupOpen((prev) => !prev);
});
}
}
prev["glucose"] = glucoseInstance;
return prev;
});
}, [animationObj, getAnimationObject, isAnimPopupOpen]);
return (
<>
<div className=" mt-8 ">
<div className="mt-8">
<p
className="text-2xl bg-red-200 font-primary border border-red-800 w-max justify-self-center inline p-4 animate-fade-in"
>
......@@ -40,7 +61,7 @@ const Opening = () => {
Diabetes is a disease in which the body is unable to produce a sufficient amount of&nbsp;
<span
className="clickable-text"
onClick={togglePopup}
onClick={() => setIsPopupOpen(!isPopupOpen)}
style={{textDecoration: 'underline', cursor: 'pointer'}}
>
insulin
......@@ -51,7 +72,7 @@ const Opening = () => {
Cukrovka je ochorenie, pri ktorom telo nedokáže produkovať dostatočné množstvo&nbsp;
<span
className="clickable-text"
onClick={togglePopup}
onClick={() => setIsPopupOpen(!isPopupOpen)}
style={{textDecoration: 'underline', cursor: 'pointer'}}
>
inzulínu
......@@ -61,29 +82,33 @@ const Opening = () => {
</p>
</div>
{isPopupOpen && (
<InsulinExplanationPopup
explanation={insulinExplanationText}
/>
)}
<div className="flex items-center justify-center">
<div className="w-screen">
<div className="text mt-4">
<div id="intro-text" className="font-speech max-w-md"
style={{position: 'absolute', top: 300, left: 150, right: 900}}>
<Typewriter
options={{
delay: 40,
}}
onInit={(typewriter) => onInit({typewriter, pauseFor: 3400})}
/>
</div>
<div className="flex justify-center">
<div
id="animation-container"
>
<div className="text mt-8">
<div className="flex relative justify-center">
<div id="animation-container">
<div id="intro-text" className="font-speech max-w-md absolute top-[15%] left-[10%]">
<Typewriter
options={{
delay: 40,
}}
onInit={(typewriter) => onInit({typewriter, pauseFor: 3400})}
/>
</div>
<div>
{isPopupOpen && (
<ExplanationPopup
className="absolute top-5 right-5 bg-white p-4 border border-gray-300 rounded shadow-lg max-w-xs z-10 flex"
explanation={insulinExplanationText}
/>
)}
{isAnimPopupOpen && (
<ExplanationPopup
className="absolute top-64 right-15 right-4 bg-white p-4 border border-gray-300 rounded shadow-lg max-w-xs z-10 flex"
explanation={glucoseExplanationText}
/>
)}
</div>
<AnimateCC
animationName="opening"
getAnimationObject={getAnimationObject}
......@@ -92,6 +117,7 @@ const Opening = () => {
</div>
</div>
</div>
<div className="flex justify-evenly">
<button
onClick={handleClick}
......
import {useEffect, useState} from "react";
import {Droppable, DroppableProps} from "react-beautiful-dnd";
// StrictModeDroppable.tsx
// Credits to https://github.com/GiovanniACamacho and https://github.com/Meligy for the TypeScript version
// Original post: https://github.com/atlassian/react-beautiful-dnd/issues/2399#issuecomment-1175638194
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) {
return null;
}
return <Droppable {...props}>{children}</Droppable>;
};
......@@ -19,7 +19,7 @@ interface AnimateProps {
export const useAnimate = ({link, text, typewriterRef, audioFiles}: AnimateProps) => {
const language = useRecoilValue(languageAtom);
const [paused, setPaused] = useState(false);
const [, setAnimationObject] = useState(null);
const [animationObj, setAnimationObject] = useState<any>(null);
const getAnimationObject = (obj: SetStateAction<null>) => setAnimationObject(obj);
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
......@@ -76,7 +76,7 @@ export const useAnimate = ({link, text, typewriterRef, audioFiles}: AnimateProps
.pauseFor(pauseFor)
.callFunction(() => {
if (audioRef.current) {
audioRef.current.play();
// audioRef.current.play();
}
})
.typeString(line[language as keyof Texts])
......@@ -95,6 +95,7 @@ export const useAnimate = ({link, text, typewriterRef, audioFiles}: AnimateProps
handleClick,
onInit,
handleMuteToggle,
isMuted
isMuted,
animationObj
}
}
......@@ -111,3 +111,49 @@ strong {
transform: rotate(360deg);
}
}
/* target the specific div created by react-adobe-animate package when rendering animation */
/*html body div > div > div > div > div:nth-child(1) > div:nth-child(2) > div > div > div > div {*/
/* max-height: 100%;*/
/* max-width: 100%;*/
/*}*/
body {
overflow: hidden;
}
.glucoseTimeline {
list-style: none;
padding-left: 0;
}
.glucoseTimeline li {
display: flex;
align-items: center;
border: solid 2px #5599ff;
border-radius: .2em;
padding: .5em .8em .5em .5em;
margin-bottom: .75em;
background-color: #ffffff;
}
.glucoseTimeline p {
max-width: none;
font-weight: bold;
margin: 0;
}
.timeline-image {
overflow: hidden;
flex-shrink: 0;
width: 4em;
height: 4em;
background-color: #aaccff;
padding: .5em;
margin-right: .5em;
}
.timeline-image img {
display: block;
width: 100%;
height: auto;
}
......@@ -6,7 +6,6 @@ export default {
primary: {
light: '#5599ff',
main: '#3D348B',
// main: '#B7F0AD',
dark: '#aaccff',
},
beige: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment