-u level & exercise hooks
This commit is contained in:
parent
6a48573052
commit
be9aba3355
|
|
@ -21,8 +21,6 @@ import { SlugProvider } from '../../utils/SlugContext';
|
|||
import '../../assets/styles/user.css';
|
||||
import useAuth from '../guest/auth/hooks/useAuth';
|
||||
|
||||
import ExerciseTest from './exerciseCombine/views/Exercise';
|
||||
|
||||
const UserRoutes = () => {
|
||||
const { logout } = useAuth();
|
||||
return (
|
||||
|
|
@ -44,8 +42,6 @@ const UserRoutes = () => {
|
|||
<Route path="module/:section/:topic/:level/material" element={<Material />} />
|
||||
<Route path="module/:section/:topic/:level/exercise" element={<Exercise />} />
|
||||
<Route path="module/:section/:topic/:level/result" element={<ExerciseResult />} />
|
||||
|
||||
<Route path="module/:section/:topic/:level/exercise-test" element={<ExerciseTest />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</UserLayout>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const useExercises = (topic, level) => {
|
|||
|
||||
const data = await exerciseService.fetchExercise(getLevelId);
|
||||
setQuestions(data);
|
||||
if (localStorage.getItem(getLevelId)) {
|
||||
if (localStorage.getItem(stdLearning.payload.ID_STUDENT_LEARNING)) {
|
||||
// const savedAnswers = JSON.parse(localStorage.getItem(getLevelId));
|
||||
const savedAnswers = JSON.parse(localStorage.getItem(stdLearning.payload.ID_STUDENT_LEARNING));
|
||||
setAnswers(savedAnswers);
|
||||
|
|
@ -92,7 +92,7 @@ export const useExercises = (topic, level) => {
|
|||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
const answer = JSON.parse(localStorage.getItem(levelId));
|
||||
const answer = JSON.parse(localStorage.getItem(learningId));
|
||||
try {
|
||||
const sendAnswer = await exerciseService.sumbitAnswer({ answers: answer });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ const fetchExercise = async (idLevel) => {
|
|||
|
||||
return sortedData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching exercise data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const Exercise = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (error) return <p>Error loading questions.</p>;
|
||||
if (error) return <h1 className='text-center'>Exercise questions not yet available</h1>;
|
||||
|
||||
return (
|
||||
<Container fluid className='exercise-page'>
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import exerciseService from '../services/exerciseService';
|
||||
import { useSlugContext } from '../../../../utils/SlugContext';
|
||||
import { unSlugify } from '../../../../utils/Constant';
|
||||
|
||||
export const useExercise = (topic, level) => {
|
||||
const [questions, setQuestions] = useState([]);
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [levelId, setLevelId] = useState(null);
|
||||
const [learningId, setLearningId] = useState(null);
|
||||
const { topicSlugMap } = useSlugContext();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const getLevelId = await exerciseService.getLevelId(topicSlugMap[topic], unSlugify(level));
|
||||
setLevelId(getLevelId);
|
||||
const stdLearning = await exerciseService.checkStdLearning(getLevelId);
|
||||
if (stdLearning === null) {
|
||||
const newLearningId = await exerciseService.createStdLearning({ ID_LEVEL: getLevelId });
|
||||
setLearningId(newLearningId);
|
||||
}else{
|
||||
setLearningId(stdLearning);
|
||||
}
|
||||
|
||||
|
||||
const data = await exerciseService.fetchExercise(getLevelId);
|
||||
setQuestions(data);
|
||||
if (localStorage.getItem(getLevelId)) {
|
||||
const savedAnswers = JSON.parse(localStorage.getItem(getLevelId));
|
||||
setAnswers(savedAnswers);
|
||||
}else{
|
||||
setAnswers(new Array(data.length).fill(null));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("something wrong : ", error);
|
||||
setError(error);
|
||||
}finally{
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [topic, level]);
|
||||
|
||||
const handleAnswer = (index, answer) => {
|
||||
// const newAnswers = { ...answers, [index]: answer };
|
||||
const newAnswers = [...answers];
|
||||
newAnswers[index] = answer;
|
||||
setAnswers(newAnswers);
|
||||
localStorage.setItem(levelId, JSON.stringify(newAnswers));
|
||||
};
|
||||
|
||||
const handleMatchingPairs = (questionId, pairs) => {
|
||||
const isComplete = pairs.every(pair => pair.left && pair.right);
|
||||
if (isComplete) {
|
||||
handleAnswer(questionId, pairs);
|
||||
}
|
||||
};
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (currentQuestionIndex < questions.length - 1) {
|
||||
setCurrentQuestionIndex(currentQuestionIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevQuestion = () => {
|
||||
if (currentQuestionIndex > 0) {
|
||||
setCurrentQuestionIndex(currentQuestionIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
prevQuestion();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
nextQuestion();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [prevQuestion, nextQuestion]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const allAnswered = answers.every((answer) => answer !== null);
|
||||
return allAnswered;
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = () => {
|
||||
setShowModal(false);
|
||||
navigate(`/learning/module/${section}/${topic}/${level}/result`, {
|
||||
state: { answers }
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
questions,
|
||||
currentQuestion: questions[currentQuestionIndex],
|
||||
currentQuestionIndex,
|
||||
setCurrentQuestionIndex,
|
||||
answers,
|
||||
isLoading,
|
||||
error,
|
||||
handleAnswer,
|
||||
handleMatchingPairs,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
handleSubmit,
|
||||
handleConfirmSubmit
|
||||
};
|
||||
};
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { API_URL } from '../../../../utils/Constant';
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('token')
|
||||
}
|
||||
};
|
||||
|
||||
const getSoalNumber = (title) => {
|
||||
const match = title.match(/\d+$/); // Mencari angka di akhir string
|
||||
return match ? parseInt(match[0], 10) : 0; // Mengembalikan angka atau 0 jika tidak ditemukan
|
||||
};
|
||||
|
||||
// Fungsi untuk cek apakah std_learning sudah ada
|
||||
const checkStdLearning = async (level) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/stdLearning/level/${level}`, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// console.error('Error checking std_learning:', error);
|
||||
// throw error;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelId = async (topicId, levelName) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/level/topic/${topicId}`, config);
|
||||
const filteredData = response.data.data.levels.filter(item => item.NAME_LEVEL === levelName);
|
||||
return filteredData[0].ID_LEVEL;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk membuat std_learning
|
||||
const createStdLearning = async (data) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/stdLearning`, data, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating std_learning:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk mendapatkan data exercise
|
||||
const fetchExercise = async (idLevel) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/exercise/level/${idLevel}`, config);
|
||||
// return response.data;
|
||||
|
||||
const sortedData = response.data.payload.sort((a, b) => {
|
||||
return getSoalNumber(a.TITLE) - getSoalNumber(b.TITLE);
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching exercise data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
getLevelId,
|
||||
checkStdLearning,
|
||||
createStdLearning,
|
||||
fetchExercise
|
||||
};
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useExercise } from '../hooks/useExercises';
|
||||
import { Container, Row, Col, ListGroup, Button, Modal, Alert, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import MultipleChoiceQuestion from './components/MultipleChoiceQuestion';
|
||||
import TrueFalseQuestion from './components/TrueFalseQuestion';
|
||||
import MatchingPairsQuestion from './components/MatchingPairsQuestion';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import ilustration from '../../../../assets/images/illustration/submitExercise.png';
|
||||
|
||||
const Exercise = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const { topic, level } = useParams();
|
||||
const {
|
||||
questions,
|
||||
currentQuestion,
|
||||
currentQuestionIndex,
|
||||
setCurrentQuestionIndex,
|
||||
answers,
|
||||
isLoading,
|
||||
error,
|
||||
handleAnswer,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
handleSubmit,
|
||||
handleConfirmSubmit
|
||||
} = useExercise(topic, level);
|
||||
|
||||
const handleSubmitWrapper = () => {
|
||||
if (handleSubmit()) {
|
||||
setShowModal(true);
|
||||
} else {
|
||||
setShowAlert(true);
|
||||
}
|
||||
};
|
||||
const handleCloseModal = () => setShowModal(false);
|
||||
|
||||
const renderQuestion = () => {
|
||||
switch (currentQuestion.QUESTION_TYPE) {
|
||||
case 'MCQ':
|
||||
return (
|
||||
<MultipleChoiceQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
case 'TFQ':
|
||||
return (
|
||||
<TrueFalseQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
case 'MPQ':
|
||||
return (
|
||||
<MatchingPairsQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <p>Unknown question type.</p>;
|
||||
}
|
||||
};
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-basic">
|
||||
<Popover.Header as="h4">Tips</Popover.Header>
|
||||
<Popover.Body className='p-2'>
|
||||
<ul className='ps-3 m-0'>
|
||||
<li>Click the <strong>left arrow</strong> key to go to the previous question</li>
|
||||
<li>Click the <strong>right arrow</strong> key to go to the next question</li>
|
||||
</ul>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-2">
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' count={6} style={{height:"4vh"}} />
|
||||
</div>
|
||||
<div className="col-10">
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"5vh"}} />
|
||||
<Skeleton containerClassName='w-100' className='w-50 mb-1 rounded-3' style={{height:"20vh"}} />
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"30vh"}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) return <p>Error loading questions.</p>;
|
||||
|
||||
return (
|
||||
<Container fluid className='exercise-page'>
|
||||
<Row>
|
||||
<Col sm={2}>
|
||||
<div className='p-3 rounded-4 bg-white'>
|
||||
<div className="mb-3 d-flex justify-content-between align-items-center">
|
||||
<h4 className='mb-0 text-gd fw-bold'>Pretest</h4>
|
||||
<OverlayTrigger trigger="click" placement="right" overlay={popover}>
|
||||
<i className=" bi bi-info-circle cursor-pointer text-secondary"></i>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<ListGroup variant="flush" className='number-list'>
|
||||
{questions.map((q, index) => (
|
||||
<ListGroup.Item
|
||||
key={q.ID_ADMIN_EXERCISE}
|
||||
active={index === currentQuestionIndex}
|
||||
onClick={() => setCurrentQuestionIndex(index)}
|
||||
className={`border-0 rounded-3 number-label ${answers[index] !== null ? 'answered fw-bold' : ''}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<i className={`me-2 bi bi-circle ${answers[index] !== null ? 'd-none' : 'd-block'}`}></i>
|
||||
<i className={`me-2 bi bi-check2-circle ${answers[index] !== null ? 'd-block' : 'd-none'}`}></i>
|
||||
{index+1}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col sm={10}>
|
||||
<div className='p-4 rounded-4 bg-white'>
|
||||
<div className="pb-4 d-flex justify-content-between align-items-center">
|
||||
<Button
|
||||
variant='outline-blue'
|
||||
className={`rounded-35 ${currentQuestionIndex === 0 ? 'invisible' : 'visible'}`}
|
||||
onClick={prevQuestion}
|
||||
disabled={currentQuestionIndex === 0}
|
||||
>
|
||||
<i className="bi bi-arrow-left"></i>
|
||||
</Button>
|
||||
<h5 className='m-0'>{`Questions ${currentQuestionIndex + 1} of ${questions.length}`}</h5>
|
||||
<Button
|
||||
variant="blue"
|
||||
className={`rounded-35 ${currentQuestionIndex === questions.length - 1 ? 'd-none' : ''}`}
|
||||
onClick={nextQuestion}
|
||||
disabled={currentQuestionIndex === questions.length - 1}
|
||||
>
|
||||
Next Questions <i className="bi bi-arrow-right"></i>
|
||||
</Button>
|
||||
<Button
|
||||
variant="blue"
|
||||
className={`rounded-35 px-4 ${currentQuestionIndex === questions.length - 1 ? 'd-inline-block' : 'd-none'}`}
|
||||
onClick={handleSubmitWrapper}
|
||||
>
|
||||
Submit <i className="bi bi-send"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='p-3 border rounded-3'>
|
||||
{renderQuestion()}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Alert show={showAlert} variant="danger" className="custom-alert" onClose={() => setShowAlert(false)} dismissible>
|
||||
<Alert.Heading>ATTENTION!</Alert.Heading>
|
||||
<span>Please answer all questions before submitting.</span>
|
||||
</Alert>
|
||||
|
||||
{/* <Modal show={showModal} onHide={handleCloseModal} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Confirmation</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to submit your answer?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleCloseModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleConfirmSubmit}>
|
||||
Yes, Submit <i className="bi bi-send"></i>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal> */}
|
||||
|
||||
<Modal show={showModal} onHide={handleCloseModal} centered>
|
||||
<Modal.Body className='p-4 d-flex flex-column items-center'>
|
||||
<h4 className='mb-4 fw-bold text-dark'>Proceed with <span className='text-blue'>Submission</span>?</h4>
|
||||
<img src={ilustration} alt="" />
|
||||
<p className='my-3 text-muted fw-light'>Confirm submission? There's no going back.</p>
|
||||
<div className="mt-4 w-100 d-flex justify-content-center">
|
||||
<Button variant="outline-blue" className="w-50 py-2 px-5 mx-1 rounded-35" onClick={handleCloseModal}>Check Again</Button>
|
||||
<Button variant="gd" className="w-50 py-2 px-5 mx-1 rounded-35" onClick={handleConfirmSubmit}>Confirm</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Exercise;
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const ExerciseMedia = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const handleChange = (value) => {
|
||||
onAnswer(index, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{question.TITLE}</h3>
|
||||
<p>{question.QUESTION}</p>
|
||||
{question.VIDEO && (
|
||||
<video controls>
|
||||
<source src={question.VIDEO} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
{question.IMAGE && <img src={question.IMAGE} alt="Question" />}
|
||||
{question.AUDIO && <audio controls src={question.AUDIO}></audio>}
|
||||
<div className="options-tf">
|
||||
|
||||
<div
|
||||
className={`p-0 mb-3 form-check ${savedAnswer === '1' ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleChange('1')}
|
||||
>
|
||||
<span className="option-label">A</span>
|
||||
<span className="ms-2 option-text">TRUE</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`p-0 mb-3 form-check ${savedAnswer === '0' ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleChange('0')}
|
||||
>
|
||||
<span className="option-label">B</span>
|
||||
<span className="ms-2 option-text">FALSE</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExerciseMedia;
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// const colors = ['#E9342D', '#FACC15', '#1FBC2F', '#0090FF', '#ED27D9'];
|
||||
// const colors = ['#0090FF', '#FC6454', '#46E59A', '#FBD025', '#E355D5'];
|
||||
const colors = ['#FC6454', '#FBD025', '#46E59A', '#0090FF','#E355D5'];
|
||||
|
||||
const shuffleArray = (array) => {
|
||||
return array
|
||||
.map((value) => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value);
|
||||
};
|
||||
|
||||
function arrayToString(arr) {
|
||||
return arr.join(', ');
|
||||
}
|
||||
|
||||
function stringToArray(str) {
|
||||
return str.split(', ');
|
||||
}
|
||||
|
||||
const MatchingPairsQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const [pairs, setPairs] = useState([]);
|
||||
const [selectedLeft, setSelectedLeft] = useState(null);
|
||||
const [selectedRight, setSelectedRight] = useState(null);
|
||||
const [rightOptions, setRightOptions] = useState([]);
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
const [isShuffled, setIsShuffled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest('.mp-choice')) {
|
||||
setSelectedLeft(null);
|
||||
setSelectedRight(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const initialPairs = question.matchingPairs.map((pair) => ({
|
||||
left: pair.LEFT_PAIR,
|
||||
right: '',
|
||||
color: colors[question.matchingPairs.indexOf(pair) % colors.length],
|
||||
}));
|
||||
|
||||
if (savedAnswer) {
|
||||
const arrSavedAnswer = stringToArray(savedAnswer);
|
||||
const updatedPairs = initialPairs.map((pair, index) => ({
|
||||
...pair,
|
||||
right: arrSavedAnswer[index],
|
||||
}));
|
||||
setPairs(updatedPairs);
|
||||
} else {
|
||||
setPairs(initialPairs);
|
||||
}
|
||||
|
||||
if (!isShuffled) {
|
||||
setIsShuffled(true);
|
||||
setRightOptions(shuffleArray(question.matchingPairs.map((pair) => pair.RIGHT_PAIR)));
|
||||
}
|
||||
}, [question, savedAnswer]);
|
||||
|
||||
const handleLeftClick = (index) => {
|
||||
setSelectedLeft(index);
|
||||
const status = pairs.findIndex(item => item.right === rightOptions[selectedRight]);
|
||||
if (selectedRight !== null) {
|
||||
makePair(index, selectedRight, status);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (index) => {
|
||||
setSelectedRight(index);
|
||||
const status = pairs.findIndex(item => item.right === rightOptions[index]);
|
||||
if (selectedLeft !== null) {
|
||||
makePair(selectedLeft, index, status);
|
||||
}
|
||||
};
|
||||
|
||||
const makePair = (leftIndex, rightIndex, changePair) => {
|
||||
const newPairs = [...pairs];
|
||||
if (changePair > -1) {
|
||||
setIsComplete(false);
|
||||
pairs[changePair].right = '';
|
||||
}
|
||||
|
||||
const selectedRightValue = rightOptions[rightIndex];
|
||||
newPairs[leftIndex].right = selectedRightValue;
|
||||
setPairs(newPairs);
|
||||
// console.log(newPairs);
|
||||
|
||||
setSelectedLeft(null);
|
||||
setSelectedRight(null);
|
||||
|
||||
const allPairsMatched = newPairs.every((pair) => pair.right !== '');
|
||||
if (allPairsMatched && !isComplete) {
|
||||
setIsComplete(true);
|
||||
|
||||
const rightAnswers = newPairs.map((pair) => pair.right);
|
||||
console.log(rightAnswers);
|
||||
onAnswer(index, arrayToString(rightAnswers));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{question.IMAGE !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <img src={question.IMAGE} alt="" /> */}
|
||||
<h3>{question.IMAGE}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.AUDIO !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <audio controls>
|
||||
<source src={question.AUDIO} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio> */}
|
||||
<h3>{question.AUDIO}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.VIDEO !== null && (
|
||||
// <video src={question.VIDEO} controls className='my-1'></video>
|
||||
<div className="my-1">
|
||||
<h3>{question.VIDEO}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p>{question.QUESTION}</p>
|
||||
<div className="w-100 options-mp d-flex justify-content-between">
|
||||
{/* Bagian kiri */}
|
||||
<div>
|
||||
{pairs.map((pair, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-0 mb-3 form-check mp-choice`}
|
||||
onClick={() => handleLeftClick(index)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="option-label"
|
||||
style={{
|
||||
color: "#fff",
|
||||
backgroundColor: pair.color,
|
||||
border: selectedLeft === index ? '2px dashed #ffffff' : '2px solid #ffffff',
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span
|
||||
className="ms-2 option-text"
|
||||
style={{
|
||||
color:
|
||||
selectedLeft === index
|
||||
? "#ffffff"
|
||||
: (pair.right === '' ? '#000000' : "#ffffff"),
|
||||
backgroundColor:
|
||||
selectedLeft === index
|
||||
? pair.color
|
||||
: (pair.right === '' ? '#ffffff' : pair.color),
|
||||
border:
|
||||
selectedLeft === index
|
||||
? '2px dashed #ffffff'
|
||||
: (pair.right === '' ? '1px solid #000000' : '2px solid #ffffff'),
|
||||
}}
|
||||
>
|
||||
{pair.left}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bagian kanan */}
|
||||
<div>
|
||||
{rightOptions.map((right, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-0 mb-3 form-check mp-choice`}
|
||||
onClick={() => handleRightClick(index)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="option-label"
|
||||
style={{
|
||||
color: pairs.find((pair) => pair.right === right) ? '#ffffff' : '#000000',
|
||||
backgroundColor:
|
||||
pairs.find((pair) => pair.right === right)?.color ||
|
||||
(selectedRight === index ? '#ccc' : '#ffffff'),
|
||||
border:
|
||||
pairs.find((pair) => pair.right === right)?.color ||
|
||||
(selectedRight === index ? '2px dashed #ffffff' : '1px solid #000000'),
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span
|
||||
className="ms-2 option-text"
|
||||
style={{
|
||||
color: (pairs.find((pair) => pair.right === right) ? '#ffffff' : '#000000'),
|
||||
backgroundColor:
|
||||
pairs.find((pair) => pair.right === right)?.color ||
|
||||
(selectedRight === index ? '#ccc' : '#ffffff'),
|
||||
border:
|
||||
pairs.find((pair) => pair.right === right)?.color ||
|
||||
(selectedRight === index ? '2px dashed #ffffff' : '1px solid #000000'),
|
||||
}}
|
||||
>
|
||||
{right}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchingPairsQuestion;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const MultipleChoiceQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const options = question.multipleChoices[0];
|
||||
|
||||
const handleChange = (value) => {
|
||||
onAnswer(index, value);
|
||||
};
|
||||
|
||||
function getOptionLetter(option) {
|
||||
const match = option.match(/OPTION_(\w)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{question.IMAGE !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <img src={question.IMAGE} alt="" /> */}
|
||||
<h3>{question.IMAGE}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.AUDIO !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <audio controls>
|
||||
<source src={question.AUDIO} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio> */}
|
||||
<h3>{question.AUDIO}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.VIDEO !== null && (
|
||||
// <video src={question.VIDEO} controls className='my-1'></video>
|
||||
<div className="my-1">
|
||||
<h3>{question.VIDEO}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p>{question.QUESTION}</p>
|
||||
<div className="options">
|
||||
{['OPTION_A', 'OPTION_B', 'OPTION_C', 'OPTION_D', 'OPTION_E'].map(
|
||||
(optionKey) => {
|
||||
if (options[optionKey]) {
|
||||
return (
|
||||
<div
|
||||
key={optionKey}
|
||||
className={`p-0 mb-3 form-check ${savedAnswer === getOptionLetter(optionKey) ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleChange(getOptionLetter(optionKey))}
|
||||
>
|
||||
<span className="option-label">{getOptionLetter(optionKey)}</span>
|
||||
<span className="ms-2 option-text">{options[optionKey]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultipleChoiceQuestion;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const TrueFalseQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const handleChange = (value) => {
|
||||
onAnswer(index, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{question.IMAGE !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <img src={question.IMAGE} alt="" /> */}
|
||||
<h3>{question.IMAGE}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.AUDIO !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <audio controls>
|
||||
<source src={question.AUDIO} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio> */}
|
||||
<h3>{question.AUDIO}</h3>
|
||||
</div>
|
||||
)}
|
||||
{question.VIDEO !== null && (
|
||||
// <video src={question.VIDEO} controls className='my-1'></video>
|
||||
<div className="my-1">
|
||||
<h3>{question.VIDEO}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p>{question.QUESTION}</p>
|
||||
<div className="options-tf">
|
||||
|
||||
<div
|
||||
className={`p-0 mb-3 form-check ${savedAnswer === '1' ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleChange('1')}
|
||||
>
|
||||
<span className="option-label">A</span>
|
||||
<span className="ms-2 option-text">TRUE</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`p-0 mb-3 form-check ${savedAnswer === '0' ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleChange('0')}
|
||||
>
|
||||
<span className="option-label">B</span>
|
||||
<span className="ms-2 option-text">FALSE</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrueFalseQuestion;
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import exerciseService from '../services/exerciseService';
|
||||
import { useSlugContext } from '../../../../utils/SlugContext';
|
||||
import { unSlugify } from '../../../../utils/Constant';
|
||||
|
||||
import Popover from 'react-bootstrap/Popover';
|
||||
|
||||
import image from '../../../../assets/images/illustration/material-image.png';
|
||||
import audio from '../../../../assets/audio/sample.mp3';
|
||||
import video from '../../../../assets/video/sample.mp4';
|
||||
|
||||
const basicQuestions = [
|
||||
{
|
||||
id: 1,
|
||||
question: "Which sentence is correct?",
|
||||
options: ["She don't like apples.", "She doesn't like apples.", "She not likes apples.", "She don't likes apples."],
|
||||
source: null,
|
||||
correctAnswer: 1,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: "Choose the correct form of the verb: 'He ___ to the store yesterday.'",
|
||||
options: ["go", "went", "going", "goes"],
|
||||
source: image,
|
||||
correctAnswer: 1,
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: "Select the correct sentence:",
|
||||
options: ["They was playing football.", "They were playing football.", "They playing football.", "They played football were."],
|
||||
source: audio,
|
||||
correctAnswer: 1,
|
||||
type: 'audio'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: "Fill in the blank: 'I have ___ new car.'",
|
||||
options: ["a", "an", "the", "some"],
|
||||
source: video,
|
||||
correctAnswer: 1,
|
||||
type: 'video'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
question: "Which sentence is in the past perfect tense?",
|
||||
options: ["I will have finished my homework.", "I had finished my homework.", "I finished my homework.", "I am finishing my homework."],
|
||||
source: null,
|
||||
correctAnswer: 1,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
question: "Choose the correct pronoun: 'Neither of the boys lost ___ keys.'",
|
||||
options: ["his", "their", "her", "its"],
|
||||
source: null,
|
||||
correctAnswer: 1,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
question: "Identify the error in the sentence: 'She can sings very well.'",
|
||||
options: ["sings", "can", "very", "well"],
|
||||
source: null,
|
||||
correctAnswer: 0,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
question: "Select the correct sentence:",
|
||||
options: ["There is many apples on the table.", "There are many apples on the table.", "There are much apples on the table.", "There is much apples on the table."],
|
||||
source: null,
|
||||
correctAnswer: 1,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
question: "Choose the correct conditional form: 'If it rains, we ___ inside.'",
|
||||
options: ["stay", "stays", "will stay", "stayed"],
|
||||
source: null,
|
||||
correctAnswer: 2,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
question: "Fill in the blank: 'She ___ to the gym every day.'",
|
||||
options: ["when", "going", "gone", "goes"],
|
||||
source: null,
|
||||
correctAnswer: 3,
|
||||
type: 'text'
|
||||
}
|
||||
];
|
||||
|
||||
export const useExercises = ( topic, level ) => {
|
||||
const [questions, setQuestions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [answers, setAnswers] = useState([]);
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0);
|
||||
const { topicSlugMap } = useSlugContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const checkingData = async () => {
|
||||
let levelId ='';
|
||||
try {
|
||||
levelId = await exerciseService.getLevelId(topicSlugMap[topic], unSlugify(level));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch questions:", error);
|
||||
}finally{
|
||||
const datas = await exerciseService.latestStdLearning(topicSlugMap[topic], levelId);
|
||||
if (datas === null) {
|
||||
const createLearning = await exerciseService.createStdLearning({ ID_LEVEL: levelId });
|
||||
if (createLearning.data.statusCode === 201) {
|
||||
fetchData();
|
||||
}else{
|
||||
checkingData();
|
||||
}
|
||||
}else{
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const levelId = await exerciseService.getLevelId(topicSlugMap[topic], unSlugify(level));
|
||||
const stdLearning = await exerciseService.latestStdLearning(topicSlugMap[topic], levelId);
|
||||
if (stdLearning === null) {
|
||||
await exerciseService.createStdLearning({ ID_LEVEL: levelId });
|
||||
}
|
||||
|
||||
const datas = await exerciseService.fetchExercise(levelId);
|
||||
console.log(datas);
|
||||
|
||||
// const data = allQuestions[topic]?.[level] || [];
|
||||
const data = basicQuestions || [];
|
||||
// setQuestions(data);
|
||||
|
||||
setQuestions(datas);
|
||||
setAnswers(new Array(data.length).fill(null));
|
||||
} catch (error) {
|
||||
console.error("something wrong : ", error);
|
||||
setError(error);
|
||||
}finally{
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// checkingData();
|
||||
fetchData();
|
||||
}, [topic, level]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedAnswers = JSON.parse(localStorage.getItem('answers'));
|
||||
if (savedAnswers) {
|
||||
setAnswers(savedAnswers);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (answers.some(answer => answer !== null)) {
|
||||
localStorage.setItem('answers', JSON.stringify(answers));
|
||||
console.log(localStorage.getItem('answers'));
|
||||
}
|
||||
}, [answers]);
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-basic">
|
||||
<Popover.Header as="h4">Tips</Popover.Header>
|
||||
<Popover.Body className='p-2'>
|
||||
{/* Click the <strong>left arrow</strong> key to go to the previous question */}
|
||||
{/* Click the <strong>right arrow</strong> key to go to the next question */}
|
||||
<ul className='ps-3 m-0'>
|
||||
<li>Click the <strong>left arrow</strong> key to go to the previous question</li>
|
||||
<li>Click the <strong>right arrow</strong> key to go to the next question</li>
|
||||
</ul>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
const handleAnswerSelect = (index) => {
|
||||
const newAnswers = [...answers];
|
||||
newAnswers[currentQuestion] = index;
|
||||
setAnswers(newAnswers);
|
||||
};
|
||||
|
||||
const handleNextQuestion = () => {
|
||||
setCurrentQuestion((prev) => (prev < questions.length - 1 ? prev + 1 : prev));
|
||||
};
|
||||
|
||||
const handlePrevQuestion = () => {
|
||||
setCurrentQuestion((prev) => (prev > 0 ? prev - 1 : prev));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
handlePrevQuestion();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
handleNextQuestion();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handlePrevQuestion, handleNextQuestion]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const allAnswered = answers.every((answer) => answer !== null);
|
||||
return allAnswered;
|
||||
};
|
||||
|
||||
const getCurrentQuestionData = () => {
|
||||
return questions[currentQuestion] || {};
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = () => {
|
||||
setShowModal(false);
|
||||
navigate(`/learning/module/${section}/${topic}/${level}/result`, {
|
||||
state: { answers }
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
questions,
|
||||
loading,
|
||||
error,
|
||||
answers,
|
||||
currentQuestion,
|
||||
popover,
|
||||
setAnswers,
|
||||
setCurrentQuestion,
|
||||
handleAnswerSelect,
|
||||
handleNextQuestion,
|
||||
handlePrevQuestion,
|
||||
handleSubmit,
|
||||
getCurrentQuestionData,
|
||||
handleConfirmSubmit
|
||||
};
|
||||
};
|
||||
|
||||
export default useExercises;
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const useMatchingPairs = () => {
|
||||
const COLORS = ['#E9342D', '#FACC15', '#1FBC2F', '#0090FF', '#ED27D9'];
|
||||
const [rightShuffled, setRightShuffled] = useState([]);
|
||||
const [selectedLeft, setSelectedLeft] = useState(null);
|
||||
const [selectedRight, setSelectedRight] = useState(null);
|
||||
const [matchedPairs, setMatchedPairs] = useState([]);
|
||||
const [colorMap, setColorMap] = useState({});
|
||||
const leftItems = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
|
||||
const rightItems = ['Item A', 'Item B', 'Item C', 'Item D', 'Item E'];
|
||||
const [shuffle, setShuffle] = useState(true);
|
||||
|
||||
// Mengacak bagian kanan ketika komponen pertama kali dirender
|
||||
useEffect(() => {
|
||||
if (shuffle) {
|
||||
shuffleRightItems();
|
||||
}
|
||||
}, [rightItems]);
|
||||
|
||||
// Fungsi untuk mengacak urutan bagian kanan
|
||||
const shuffleRightItems = () => {
|
||||
const shuffled = [...rightItems].sort(() => Math.random() - (leftItems.length/10));
|
||||
setRightShuffled(shuffled);
|
||||
setShuffle(false);
|
||||
};
|
||||
|
||||
// Fungsi untuk menangani klik pada bagian kiri
|
||||
const handleLeftChoose = (index) => {
|
||||
setSelectedLeft(index);
|
||||
|
||||
if (selectedRight !== null) {
|
||||
matchPair(index, selectedRight);
|
||||
resetSelections();
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk menangani klik pada bagian kanan
|
||||
const handleRightChoose = (index) => {
|
||||
setSelectedRight(index);
|
||||
|
||||
if (selectedLeft !== null) {
|
||||
matchPair(selectedLeft, index);
|
||||
resetSelections();
|
||||
}
|
||||
console.log(matchedPairs);
|
||||
};
|
||||
|
||||
// Fungsi untuk mencocokkan pasangan
|
||||
const matchPair = (leftIndex, rightIndex) => {
|
||||
if (!matchedPairs[leftIndex]) {
|
||||
const newMatchedPairs = [...matchedPairs];
|
||||
newMatchedPairs[leftIndex] = rightShuffled[rightIndex];
|
||||
|
||||
const colorIndex = leftIndex % COLORS.length;
|
||||
setColorMap((prevMap) => ({
|
||||
...prevMap,
|
||||
[leftIndex]: COLORS[colorIndex],
|
||||
}));
|
||||
|
||||
setMatchedPairs(newMatchedPairs);
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk mereset seleksi setelah pencocokan
|
||||
const resetSelections = () => {
|
||||
setSelectedLeft(null);
|
||||
setSelectedRight(null);
|
||||
};
|
||||
|
||||
const resetMatchPair = () => {
|
||||
setMatchedPairs([]);
|
||||
setColorMap({});
|
||||
resetSelections();
|
||||
};
|
||||
|
||||
return (
|
||||
rightShuffled,
|
||||
colorMap,
|
||||
handleLeftChoose,
|
||||
handleRightChoose,
|
||||
matchPair,
|
||||
resetSelections,
|
||||
resetMatchPair
|
||||
);
|
||||
};
|
||||
|
||||
export default useMatchingPairs;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useExercises from './useExercises';
|
||||
|
||||
// correct answer
|
||||
// 1. b
|
||||
// 2. b
|
||||
// 3. b
|
||||
// 4. b
|
||||
// 5. b
|
||||
// 6. b
|
||||
// 7. a
|
||||
// 8. b
|
||||
// 9. c
|
||||
//10. d
|
||||
|
||||
const useResults = (answers) => {
|
||||
const { level } = useParams();
|
||||
const [score, setScore] = useState(0);
|
||||
const [nextLevel, setNextLevel] = useState(1);
|
||||
const [savedLevel, setSavedLevel] = useState(JSON.parse(localStorage.getItem('savedLevels')) || []);
|
||||
const [savedScore, setSavedScore] = useState(JSON.parse(localStorage.getItem('savedScores')) || []);
|
||||
|
||||
const { questions } = useExercises();
|
||||
|
||||
const updateSavedScore = (scr) => {
|
||||
const newSavedScore = [...savedScore];
|
||||
const index = parseInt(level.slice(-1) - 1);
|
||||
newSavedScore[index] = scr;
|
||||
setSavedScore(newSavedScore);
|
||||
localStorage.setItem('savedScores', JSON.stringify(newSavedScore));
|
||||
};
|
||||
|
||||
const updateSavedLevel = (index, newValue) => {
|
||||
const newSavedLevel = [...savedLevel];
|
||||
newSavedLevel[index] = newValue;
|
||||
setSavedLevel(newSavedLevel);
|
||||
localStorage.setItem('savedLevels', JSON.stringify(newSavedLevel));
|
||||
};
|
||||
|
||||
const unlockSavedLevel = () => {
|
||||
const newSavedLevel = savedLevel.map(() => false);
|
||||
setSavedLevel(newSavedLevel);
|
||||
localStorage.setItem('savedLevels', JSON.stringify(newSavedLevel));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (answers && questions) {
|
||||
let totalScore = 0;
|
||||
|
||||
answers.forEach((answer, index) => {
|
||||
const question = questions[index];
|
||||
if (question) {
|
||||
if (answer === question.correctAnswer) {
|
||||
totalScore += 100 / questions.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setScore(totalScore);
|
||||
updateSavedScore(totalScore);
|
||||
if (totalScore >= 90 && level === 'level-5') { //all done
|
||||
setNextLevel(0);
|
||||
unlockSavedLevel();
|
||||
return;
|
||||
} else if (totalScore >= 90 && level !== 'level-5') {
|
||||
setNextLevel(5);
|
||||
updateSavedLevel(4, false);
|
||||
return;
|
||||
} else if (totalScore >= 75) {
|
||||
setNextLevel(4);
|
||||
updateSavedLevel(3, false);
|
||||
return;
|
||||
} else if (totalScore >= 60) {
|
||||
setNextLevel(3);
|
||||
updateSavedLevel(2, false);
|
||||
return;
|
||||
} else if (totalScore >= 40) {
|
||||
setNextLevel(2);
|
||||
updateSavedLevel(1, false);
|
||||
return;
|
||||
} else {
|
||||
setNextLevel(1);
|
||||
}
|
||||
|
||||
}
|
||||
}, [answers, questions]);
|
||||
|
||||
localStorage.removeItem('answers');
|
||||
return { score, nextLevel };
|
||||
};
|
||||
|
||||
export default useResults;
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { API_URL } from '../../../../utils/Constant';
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('token')
|
||||
}
|
||||
};
|
||||
|
||||
const getSoalNumber = (title) => {
|
||||
const match = title.match(/\d+$/); // Mencari angka di akhir string
|
||||
return match ? parseInt(match[0], 10) : 0; // Mengembalikan angka atau 0 jika tidak ditemukan
|
||||
};
|
||||
|
||||
// Fungsi untuk cek apakah std_learning sudah ada
|
||||
const latestStdLearning = async (topic, level) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/stdLearning/level/${level}`, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// console.error('Error checking std_learning:', error);
|
||||
// throw error;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelId = async (topicId, levelName) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/level/topic/${topicId}`, config);
|
||||
const filteredData = response.data.data.levels.filter(item => item.NAME_LEVEL === levelName);
|
||||
return filteredData[0].ID_LEVEL;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Fungsi untuk cek apakah std_learning sudah ada
|
||||
const checkStdLearning = async (token) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/std_learning`, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error checking std_learning:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk membuat std_learning
|
||||
const createStdLearning = async (data) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/stdLearning`, data, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating std_learning:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk mendapatkan data exercise
|
||||
const fetchExercise = async (idLevel) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/exercise/level/${idLevel}`, config);
|
||||
// return response.data;
|
||||
|
||||
const sortedData = response.data.payload.sort((a, b) => {
|
||||
return getSoalNumber(a.TITLE) - getSoalNumber(b.TITLE);
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching exercise data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
latestStdLearning,
|
||||
getLevelId,
|
||||
|
||||
checkStdLearning,
|
||||
createStdLearning,
|
||||
fetchExercise
|
||||
};
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Container, Row, Col, ListGroup, Button, Modal, Alert, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import useExercises from '../hooks/useExercises';
|
||||
import ilustration from '../../../../assets/images/illustration/submitExercise.png';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import MatchingPairs from './components/MatchingPair';
|
||||
|
||||
const Exercise = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(null);
|
||||
|
||||
const { section, topic, level } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
questions,
|
||||
loading,
|
||||
error,
|
||||
answers,
|
||||
currentQuestion,
|
||||
popover,
|
||||
setAnswers,
|
||||
setCurrentQuestion,
|
||||
handleAnswerSelect,
|
||||
handleNextQuestion,
|
||||
handlePrevQuestion,
|
||||
handleSubmit,
|
||||
getCurrentQuestionData,
|
||||
handleConfirmSubmit
|
||||
} = useExercises( topic, level );
|
||||
|
||||
const handleSubmitWrapper = () => {
|
||||
if (handleSubmit()) {
|
||||
setShowModal(true);
|
||||
} else {
|
||||
setShowAlert(true);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleConfirmSubmit = () => {
|
||||
// setShowModal(false);
|
||||
// navigate(`/learning/module/${section}/${topic}/${level}/result`, {
|
||||
// state: { answers }
|
||||
// });
|
||||
// };
|
||||
|
||||
const handleCloseModal = () => setShowModal(false);
|
||||
|
||||
const currentQuestionData = getCurrentQuestionData();
|
||||
const questionId = currentQuestionData.id || '';
|
||||
|
||||
|
||||
const handleSelectLeft = (index) => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-2">
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' count={6} style={{height:"4vh"}} />
|
||||
</div>
|
||||
<div className="col-10">
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"5vh"}} />
|
||||
<Skeleton containerClassName='w-100' className='w-50 mb-1 rounded-3' style={{height:"20vh"}} />
|
||||
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"30vh"}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container fluid className='exercise-page'>
|
||||
<Row>
|
||||
<Col sm={2}>
|
||||
<div className='p-3 rounded-4 bg-white'>
|
||||
<div className="mb-3 d-flex justify-content-between align-items-center">
|
||||
<h4 className='mb-0 text-gd fw-bold'>Pretest</h4>
|
||||
<OverlayTrigger trigger="click" placement="right" overlay={popover}>
|
||||
<i className=" bi bi-info-circle cursor-pointer text-secondary"></i>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<ListGroup variant="flush" className='number-list'>
|
||||
{questions.map((q, index) => (
|
||||
<ListGroup.Item
|
||||
key={q.ID_ADMIN_EXERCISE}
|
||||
active={index === currentQuestion}
|
||||
onClick={() => setCurrentQuestion(index)}
|
||||
className={`border-0 rounded-3 number-label ${answers[index] !== null ? 'answered fw-bold' : ''}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<i className={`me-2 bi bi-circle ${answers[index] !== null ? 'd-none' : 'd-block'}`}></i>
|
||||
<i className={`me-2 bi bi-check2-circle ${answers[index] !== null ? 'd-block' : 'd-none'}`}></i>
|
||||
{index+1}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col sm={10}>
|
||||
<div className='p-4 rounded-4 bg-white'>
|
||||
<div className="pb-4 d-flex justify-content-between align-items-center">
|
||||
<Button
|
||||
variant='outline-blue'
|
||||
className={`rounded-35 ${currentQuestion === 0 ? 'invisible' : 'visible'}`}
|
||||
onClick={handlePrevQuestion}
|
||||
disabled={currentQuestion === 0}
|
||||
>
|
||||
<i className="bi bi-arrow-left"></i>
|
||||
</Button>
|
||||
<h5 className='m-0'>{`Questions ${currentQuestion + 1} of ${questions.length}`}</h5>
|
||||
<Button
|
||||
variant="blue"
|
||||
className={`rounded-35 ${currentQuestion === questions.length - 1 ? 'd-none' : ''}`}
|
||||
onClick={handleNextQuestion}
|
||||
disabled={currentQuestion === questions.length - 1}
|
||||
>
|
||||
Next Questions <i className="bi bi-arrow-right"></i>
|
||||
</Button>
|
||||
<Button
|
||||
variant="blue"
|
||||
className={`rounded-35 px-4 ${currentQuestion === questions.length - 1 ? 'd-inline-block' : 'd-none'}`}
|
||||
onClick={handleSubmitWrapper}
|
||||
>
|
||||
Submit <i className="bi bi-send"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='p-3 border rounded-3'>
|
||||
{currentQuestionData.image && (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={currentQuestionData.image}
|
||||
alt="question illustration"
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentQuestionData.IMAGE !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <img src={currentQuestionData.source} alt="" /> */}
|
||||
<h3>{currentQuestionData.IMAGE}</h3>
|
||||
</div>
|
||||
)}
|
||||
{currentQuestionData.AUDIO !== null && (
|
||||
<div className='my-1'>
|
||||
{/* <AudioPlayer audioSrc={currentQuestionData.source} /> */}
|
||||
{/* <audio controls>
|
||||
<source src={currentQuestionData.source} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio> */}
|
||||
<h3>{currentQuestionData.AUDIO}</h3>
|
||||
</div>
|
||||
)}
|
||||
{currentQuestionData.VIDEO !== null && (
|
||||
// <video src={currentQuestionData.source} controls className='my-1'></video>
|
||||
<div className="my-1">
|
||||
<h3>{currentQuestionData.VIDEO}</h3>
|
||||
</div>
|
||||
)}
|
||||
<p className='mb-4'>{currentQuestionData.QUESTION}</p>
|
||||
{currentQuestionData.QUESTION_TYPE === "MCQ" && (
|
||||
<div className="options">
|
||||
{/* {currentQuestionData.options?.map((option, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === idx ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(idx)}
|
||||
>
|
||||
<span className="option-label">{String.fromCharCode(65 + idx)}</span>
|
||||
<span className="ms-2 option-text">{option}</span>
|
||||
</div>
|
||||
))} */}
|
||||
<div
|
||||
key={0}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 0 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(0)}
|
||||
>
|
||||
<span className="option-label">A</span>
|
||||
<span className="ms-2 option-text">{currentQuestionData.multipleChoices[0].OPTION_A}</span>
|
||||
</div>
|
||||
<div
|
||||
key={1}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 1 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(1)}
|
||||
>
|
||||
<span className="option-label">B</span>
|
||||
<span className="ms-2 option-text">{currentQuestionData.multipleChoices[0].OPTION_B}</span>
|
||||
</div>
|
||||
<div
|
||||
key={2}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 2 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(2)}
|
||||
>
|
||||
<span className="option-label">C</span>
|
||||
<span className="ms-2 option-text">{currentQuestionData.multipleChoices[0].OPTION_C}</span>
|
||||
</div>
|
||||
<div
|
||||
key={3}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 3 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(3)}
|
||||
>
|
||||
<span className="option-label">D</span>
|
||||
<span className="ms-2 option-text">{currentQuestionData.multipleChoices[0].OPTION_D}</span>
|
||||
</div>
|
||||
{currentQuestionData.multipleChoices[0].OPTION_E && (
|
||||
<div
|
||||
key={4}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 4 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(4)}
|
||||
>
|
||||
<span className="option-label">E</span>
|
||||
<span className="ms-2 option-text">{currentQuestionData.multipleChoices[0].OPTION_E}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currentQuestionData.QUESTION_TYPE === "TFQ" && (
|
||||
<div className="options-tf">
|
||||
<div
|
||||
key={0}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 0 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(0)}
|
||||
>
|
||||
<span className="option-label">A</span>
|
||||
<span className="ms-2 option-text">TRUE</span>
|
||||
</div>
|
||||
<div
|
||||
key={1}
|
||||
className={`p-0 mb-3 form-check ${answers[currentQuestion] === 1 ? 'selected-answer' : ''}`}
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => handleAnswerSelect(1)}
|
||||
>
|
||||
<span className="option-label">B</span>
|
||||
<span className="ms-2 option-text">FALSE</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentQuestionData.QUESTION_TYPE === "MPQ" &&(
|
||||
// <div className="w-100 options-mp d-flex justify-content-between">
|
||||
// <div>
|
||||
// {currentQuestionData.matchingPairs?.map((option, idx) => (
|
||||
// <div
|
||||
// key={idx}
|
||||
// className={`p-0 mb-3 form-check ${answers[currentQuestion] === idx ? 'selected-answer' : ''}`}
|
||||
// style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
// onClick={() => handleLeftChoose(idx)}
|
||||
// >
|
||||
// <span className={`option-label left label-${idx + 1}${activeIndex === idx ? " active" : ""}`}>{idx + 1}</span>
|
||||
// {/* <span className="option-label">{idx + 1}</span> */}
|
||||
// {/* <span className="ms-2 option-text">{option.LEFT_PAIR}</span> */}
|
||||
// <span className={`ms-2 option-text left label-${idx + 1}${activeIndex === idx ? " active" : ""}`}>{option.LEFT_PAIR}</span>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// <div>
|
||||
// {currentQuestionData.matchingPairs?.map((option, idx) => (
|
||||
// <div
|
||||
// key={idx}
|
||||
// className={`p-0 mb-3 form-check ${answers[currentQuestion] === idx ? 'selected-answer' : ''}`}
|
||||
// style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
// onClick={() => handleRightChoose(idx)}
|
||||
// >
|
||||
// <span className={`option-label label-${idx + 1}${activeIndex === idx ? " active" : ""}`}>{idx + 1}</span>
|
||||
// {/* <span className="option-label">{idx + 1}</span> */}
|
||||
// {/* <span className="ms-2 option-text">{option.LEFT_PAIR}</span> */}
|
||||
// <span className={`ms-2 option-text label-${idx + 1}${activeIndex === idx ? " active" : ""}`}>{option.RIGHT_PAIR}</span>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
<MatchingPairs pairData={currentQuestionData.matchingPairs} numberQuestion={currentQuestion} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Alert show={showAlert} variant="danger" className="custom-alert" onClose={() => setShowAlert(false)} dismissible>
|
||||
<Alert.Heading>ATTENTION!</Alert.Heading>
|
||||
<span>Please answer all questions before submitting.</span>
|
||||
</Alert>
|
||||
|
||||
{/* <Modal show={showModal} onHide={handleCloseModal} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Confirmation</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to submit your answer?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleCloseModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleConfirmSubmit}>
|
||||
Yes, Submit <i className="bi bi-send"></i>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal> */}
|
||||
|
||||
<Modal show={showModal} onHide={handleCloseModal} centered>
|
||||
<Modal.Body className='p-4 d-flex flex-column items-center'>
|
||||
<h4 className='mb-4 fw-bold text-dark'>Proceed with <span className='text-blue'>Submission</span>?</h4>
|
||||
<img src={ilustration} alt="" />
|
||||
<p className='my-3 text-muted fw-light'>Confirm submission? There's no going back.</p>
|
||||
<div className="mt-4 w-100 d-flex justify-content-center">
|
||||
<Button variant="outline-blue" className="w-50 py-2 px-5 mx-1 rounded-35" onClick={handleCloseModal}>Check Again</Button>
|
||||
<Button variant="gd" className="w-50 py-2 px-5 mx-1 rounded-35" onClick={handleConfirmSubmit}>Confirm</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Exercise;
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
// import React from 'react';
|
||||
// import { Link, useParams, useLocation } from 'react-router-dom';
|
||||
// import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||
// import staydown from '../../assets/images/user/resultStay.png'
|
||||
// import jump from '../../assets/images/user/resultJump.png'
|
||||
// import finish from '../../assets/images/user/resultFinish.png'
|
||||
|
||||
// const ExerciseResult = () => {
|
||||
// const { section, topic } = useParams();
|
||||
// const location = useLocation();
|
||||
// localStorage.removeItem('answers');
|
||||
|
||||
// return (
|
||||
// <Container className="result-page">
|
||||
// <Row>
|
||||
|
||||
// {/* nilai sudah diatas KKM */}
|
||||
// <Col xs={12} className='p-4 bg-white rounded-4 d-flex flex-column align-items-center'>
|
||||
// <h4>Your Result!</h4>
|
||||
// <img src={jump} alt="/" className='h-50'/>
|
||||
// <p className='my-3 text-muted'>
|
||||
// You scored <span className='text-blue fw-bold'> 60/100</span>. Great job! You can jump to...
|
||||
// </p>
|
||||
// <h1 className='mb-3 display-5 text-blue fw-bold'>LEVEL 3</h1>
|
||||
// <Button as={Link} to={`/learning/${section}/${topic}`} variant='warning' className='py-2 px-5 rounded-35'>Continue</Button>
|
||||
// </Col>
|
||||
|
||||
// {/* jika nilai dibawah KKM */}
|
||||
// <Col xs={12} className='p-4 bg-white rounded-4 d-flex flex-column align-items-center'>
|
||||
// <h4>Your Result!</h4>
|
||||
// <img src={staydown} alt="/" className='h-50'/>
|
||||
// <p className='my-3 text-muted'>
|
||||
// You scored <span className='text-danger fw-bold'> 30/100</span>. Good effort! To improve, you can focus on...
|
||||
// </p>
|
||||
// <h1 className='mb-3 display-5 text-danger fw-bold'>LEVEL 3</h1>
|
||||
// <Button as={Link} to={`/learning/${section}/${topic}`} variant='warning' className='py-2 px-5 rounded-35'>Continue</Button>
|
||||
// </Col>
|
||||
|
||||
// {/* jika nilai sudah bagus tetapi belum cukup untuk ke level selanjutnya */}
|
||||
// <Col xs={12} className='p-4 bg-white rounded-4 d-flex flex-column align-items-center'>
|
||||
// <h4>Your Result!</h4>
|
||||
// <img src={staydown} alt="/" className='h-50'/>
|
||||
// <p className='my-3 text-muted text-center'>
|
||||
// You scored <span className='text-blue fw-bold'> 30/100</span>. Learning is a journey. <br />
|
||||
// Let's explore this level further to deepen your knowledge.
|
||||
// </p>
|
||||
// <h1 className='mb-3 display-5 fw-bold'>LEVEL 2</h1>
|
||||
// <Button as={Link} to={`/learning/${section}/${topic}`} variant='warning' className='py-2 px-5 rounded-35'>Continue</Button>
|
||||
// </Col>
|
||||
|
||||
// {/* jika yang telah dikerjakan adalah level 5 */}
|
||||
// <Col xs={12} className='p-4 bg-white rounded-4 d-flex flex-column align-items-center'>
|
||||
// <h4>Your Result!</h4>
|
||||
// <img src={finish} alt="/" className='h-50'/>
|
||||
// <p className='my-3 text-muted'>
|
||||
// Way to go! You conquered <span className='text-blue fw-bold'> LEVEL 5</span> with a <span className='text-blue fw-bold'> 90/100</span>! You're a rock star!
|
||||
// </p>
|
||||
// <h1 className='mb-3 display-5 fw-bold'>LEVEL 2</h1>
|
||||
// <Button as={Link} to={`/learning/${section}/${topic}`} variant='warning' className='py-2 px-5 rounded-35'>Continue</Button>
|
||||
// </Col>
|
||||
// </Row>
|
||||
// </Container>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default ExerciseResult;
|
||||
|
||||
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import { Link, useParams, useLocation } from 'react-router-dom';
|
||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||
import staydown from '../../../../assets/images/illustration/resultStay.png';
|
||||
import jump from '../../../../assets/images/illustration/resultJump.png';
|
||||
import finish from '../../../../assets/images/illustration/resultFinish.png';
|
||||
import useResults from '../hooks/useResults';
|
||||
|
||||
const ExerciseResult = () => {
|
||||
const { section, topic, level } = useParams();
|
||||
const location = useLocation();
|
||||
const { answers } = location.state || { answers: [] };
|
||||
|
||||
const { score, nextLevel } = useResults(answers);
|
||||
|
||||
let resultImage;
|
||||
let resultText;
|
||||
let resultColor;
|
||||
|
||||
if (nextLevel === 0) {
|
||||
resultImage = finish;
|
||||
resultColor = 'text-blue';
|
||||
resultText = (
|
||||
<> Way to go! You conquered <span className='text-blue fw-bold'>LEVEL 5</span> with a <span className='text-blue fw-bold'>{score}/100</span>! You're a rock star!</>
|
||||
);
|
||||
} else if (score >= 60) {
|
||||
resultImage = jump;
|
||||
resultColor = 'text-blue';
|
||||
resultText = (
|
||||
<> You scored <span className='text-blue fw-bold'>{score}/100</span>. Great job! You can jump to...</>
|
||||
);
|
||||
} else if (score >= 40) {
|
||||
resultImage = staydown;
|
||||
resultColor = '';
|
||||
resultText = (
|
||||
<> You scored <span className='text-blue fw-bold'>{score}/100</span>. Learning is a journey. <br/> Let's explore this level further to deepen your knowledge.</>
|
||||
);
|
||||
} else {
|
||||
resultImage = staydown;
|
||||
resultColor = 'text-red';
|
||||
resultText = (
|
||||
<> You scored <span className='text-red fw-bold'>{score}/100</span>. Good effort! To improve, you can focus on...</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="result-page">
|
||||
<Row>
|
||||
<Col xs={12} className='p-4 bg-white rounded-4 d-flex flex-column align-items-center'>
|
||||
<h4>Your Result!</h4>
|
||||
<img src={resultImage} alt="/" className='h-50'/>
|
||||
<p className='my-3 text-muted text-center'>
|
||||
{resultText}
|
||||
</p>
|
||||
<h1 className={`mb-3 display-5 fw-bold ${resultColor}`}>{nextLevel === 0 ? '' : `LEVEL ${nextLevel}`}</h1>
|
||||
<Button as={Link} to={`/learning/${section}/${topic}`} variant='warning' className='py-2 px-5 rounded-35'>Continue</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExerciseResult;
|
||||
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const COLORS = ['#E9342D', '#FACC15', '#1FBC2F', '#0090FF', '#ED27D9'];
|
||||
|
||||
const MatchingPairs = ( stateData ) => {
|
||||
const [selectedLeft, setSelectedLeft] = useState(null);
|
||||
const [selectedRight, setSelectedRight] = useState(null);
|
||||
const [matchedPairs, setMatchedPairs] = useState([]);
|
||||
const [colorMap, setColorMap] = useState({});
|
||||
const [leftItems, setLeftPairs] = useState([]);
|
||||
const [rightItems, setRightPairs] = useState([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log(stateData);
|
||||
const pairItem = stateData.pairData;
|
||||
const left = pairItem.map(pair => pair.LEFT_PAIR);
|
||||
const right = pairItem.map(pair => pair.RIGHT_PAIR);
|
||||
const shuffledRight = [...right].sort(() => Math.random() - (left.length/10));
|
||||
|
||||
setLeftPairs(left);
|
||||
setRightPairs(shuffledRight);
|
||||
}, []);
|
||||
|
||||
|
||||
// Fungsi untuk menangani klik pada bagian kiri
|
||||
const handleLeftClick = (index) => {
|
||||
setSelectedLeft(index);
|
||||
|
||||
if (selectedRight !== null) {
|
||||
matchPair(index, selectedRight);
|
||||
resetSelections();
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk menangani klik pada bagian kanan
|
||||
const handleRightClick = (index) => {
|
||||
setSelectedRight(index);
|
||||
|
||||
if (selectedLeft !== null) {
|
||||
matchPair(selectedLeft, index);
|
||||
resetSelections();
|
||||
}
|
||||
console.log(selectedRight);
|
||||
};
|
||||
|
||||
// Fungsi untuk mencocokkan pasangan
|
||||
const matchPair = (leftIndex, rightIndex) => {
|
||||
if (!matchedPairs[leftIndex]) {
|
||||
const newMatchedPairs = [...matchedPairs];
|
||||
newMatchedPairs[leftIndex] = rightItems[rightIndex];
|
||||
|
||||
const colorIndex = leftIndex % COLORS.length;
|
||||
setColorMap((prevMap) => ({
|
||||
...prevMap,
|
||||
[leftIndex]: COLORS[colorIndex],
|
||||
}));
|
||||
|
||||
setMatchedPairs(newMatchedPairs);
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk mereset seleksi setelah pencocokan
|
||||
const resetSelections = () => {
|
||||
setSelectedLeft(null);
|
||||
setSelectedRight(null);
|
||||
};
|
||||
|
||||
const resetAnswers = () => {
|
||||
setMatchedPairs([]);
|
||||
setColorMap({});
|
||||
resetSelections();
|
||||
};
|
||||
|
||||
const isAllMatched = () => {
|
||||
console.log('gatau');
|
||||
return matchedPairs.length === leftItems.length && matchedPairs.every((pair) => pair !== undefined && pair !== null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gatau">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '50px' }}>
|
||||
{/* Bagian Kiri */}
|
||||
<div>
|
||||
<h3>Left Items</h3>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{leftItems.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => handleLeftClick(index)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colorMap[index] || 'transparent',
|
||||
padding: '10px',
|
||||
marginBottom: '5px',
|
||||
border: selectedLeft === index ? '2px solid black' : '2px solid #fff',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Bagian Kanan */}
|
||||
<div>
|
||||
<h3>Right Items (Shuffled)</h3>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{rightItems.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => handleRightClick(index)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: matchedPairs.includes(item) ? colorMap[matchedPairs.indexOf(item)] : 'transparent',
|
||||
padding: '10px',
|
||||
marginBottom: '5px',
|
||||
border: selectedRight === index ? '2px solid black' : '2px solid #fff',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div style={{ marginTop: '20px' }}>
|
||||
{isAllMatched() ? (
|
||||
<div style={{ color: 'green' }}>Semua pasangan sudah terjawab!</div>
|
||||
) : (
|
||||
<div style={{ color: 'red' }}>Belum semua pasangan terjawab.</div>
|
||||
)}
|
||||
</div> */}
|
||||
<button onClick={resetAnswers}>reset</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchingPairs;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import exerciseService from '../services/exerciseServices';
|
||||
import { useSlugContext } from '../../../../utils/SlugContext';
|
||||
import { unSlugify } from '../../../../utils/Constant';
|
||||
|
||||
export const useExercise = (topic, level) => {
|
||||
const [questions, setQuestions] = useState([]);
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [levelId, setLevelId] = useState(null);
|
||||
const [learningId, setLearningId] = useState(null);
|
||||
const { topicSlugMap } = useSlugContext();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const getLevelId = await exerciseService.getLevelId(topicSlugMap[topic], unSlugify(level));
|
||||
setLevelId(getLevelId);
|
||||
const stdLearning = await exerciseService.checkStdLearning(getLevelId);
|
||||
if (stdLearning === null) {
|
||||
const newLearningId = await exerciseService.createStdLearning({ ID_LEVEL: getLevelId });
|
||||
setLearningId(newLearningId);
|
||||
}else{
|
||||
setLearningId(stdLearning);
|
||||
}
|
||||
|
||||
|
||||
const data = await exerciseService.fetchExercise(getLevelId);
|
||||
setQuestions(data);
|
||||
if (localStorage.getItem(getLevelId)) {
|
||||
const savedAnswers = JSON.parse(localStorage.getItem(getLevelId));
|
||||
setAnswers(savedAnswers);
|
||||
}else{
|
||||
setAnswers(new Array(data.length).fill(null));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("something wrong : ", error);
|
||||
setError(error);
|
||||
}finally{
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data soal dari API saat komponen dimount
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [topic, level]);
|
||||
|
||||
// Fungsi untuk menangani jawaban user dan menyimpannya ke localStorage
|
||||
const handleAnswer = (index, answer) => {
|
||||
// const newAnswers = { ...answers, [index]: answer };
|
||||
const newAnswers = [...answers];
|
||||
newAnswers[index] = answer;
|
||||
setAnswers(newAnswers);
|
||||
console.log(newAnswers);
|
||||
localStorage.setItem(levelId, JSON.stringify(newAnswers));
|
||||
};
|
||||
|
||||
// Fungsi untuk menyimpan pasangan jawaban Matching Pair ketika sudah lengkap
|
||||
const handleMatchingPairs = (questionId, pairs) => {
|
||||
const isComplete = pairs.every(pair => pair.left && pair.right); // Pastikan semua pasangan sudah lengkap
|
||||
if (isComplete) {
|
||||
handleAnswer(questionId, pairs); // Simpan jawaban jika sudah lengkap
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk pindah ke soal selanjutnya
|
||||
const nextQuestion = () => {
|
||||
if (currentQuestionIndex < questions.length - 1) {
|
||||
setCurrentQuestionIndex(currentQuestionIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk kembali ke soal sebelumnya
|
||||
const prevQuestion = () => {
|
||||
if (currentQuestionIndex > 0) {
|
||||
setCurrentQuestionIndex(currentQuestionIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
questions,
|
||||
currentQuestion: questions[currentQuestionIndex],
|
||||
currentQuestionIndex,
|
||||
setCurrentQuestionIndex,
|
||||
answers,
|
||||
isLoading,
|
||||
error,
|
||||
handleAnswer,
|
||||
handleMatchingPairs,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { API_URL } from '../../../../utils/Constant';
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('token')
|
||||
}
|
||||
};
|
||||
|
||||
const getSoalNumber = (title) => {
|
||||
const match = title.match(/\d+$/); // Mencari angka di akhir string
|
||||
return match ? parseInt(match[0], 10) : 0; // Mengembalikan angka atau 0 jika tidak ditemukan
|
||||
};
|
||||
|
||||
// Fungsi untuk cek apakah std_learning sudah ada
|
||||
const checkStdLearning = async (level) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/stdLearning/level/${level}`, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// console.error('Error checking std_learning:', error);
|
||||
// throw error;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelId = async (topicId, levelName) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/level/topic/${topicId}`, config);
|
||||
const filteredData = response.data.data.levels.filter(item => item.NAME_LEVEL === levelName);
|
||||
return filteredData[0].ID_LEVEL;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk membuat std_learning
|
||||
const createStdLearning = async (data) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/stdLearning`, data, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating std_learning:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Fungsi untuk mendapatkan data exercise
|
||||
const fetchExercise = async (idLevel) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/exercise/level/${idLevel}`, config);
|
||||
// return response.data;
|
||||
|
||||
const sortedData = response.data.payload.sort((a, b) => {
|
||||
return getSoalNumber(a.TITLE) - getSoalNumber(b.TITLE);
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching exercise data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
getLevelId,
|
||||
checkStdLearning,
|
||||
createStdLearning,
|
||||
fetchExercise
|
||||
};
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useExercise } from '../hooks/useExercise';
|
||||
import MultipleChoiceQuestion from './MultipleChoiceQuestion';
|
||||
import TrueFalseQuestion from './TrueFalseQuestion';
|
||||
import MatchingPairsQuestion from './MatchingPairsQuestion';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const Exercise = () => {
|
||||
const { topic, level } = useParams();
|
||||
const {
|
||||
questions,
|
||||
currentQuestion,
|
||||
currentQuestionIndex,
|
||||
setCurrentQuestionIndex,
|
||||
answers,
|
||||
isLoading,
|
||||
error,
|
||||
handleAnswer,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
} = useExercise(topic, level);
|
||||
|
||||
if (isLoading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error loading questions.</p>;
|
||||
|
||||
const renderQuestion = () => {
|
||||
switch (currentQuestion.QUESTION_TYPE) {
|
||||
case 'MCQ':
|
||||
return (
|
||||
<MultipleChoiceQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
case 'TFQ':
|
||||
return (
|
||||
<TrueFalseQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
case 'MPQ':
|
||||
return (
|
||||
<MatchingPairsQuestion
|
||||
question={currentQuestion}
|
||||
onAnswer={handleAnswer}
|
||||
savedAnswer={answers[currentQuestionIndex]}
|
||||
index={currentQuestionIndex}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <p>Unknown question type.</p>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Exercise</h2>
|
||||
<div>
|
||||
{/* List nomor soal */}
|
||||
<div>
|
||||
{questions.map((q, index) => (
|
||||
<button
|
||||
key={q.ID_ADMIN_EXERCISE}
|
||||
onClick={() => setCurrentQuestionIndex(index)}
|
||||
style={{
|
||||
fontWeight: index === currentQuestionIndex ? 'bold' : 'normal',
|
||||
color: answers[index] !== null ? 'red' : '#000',
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Soal saat ini */}
|
||||
<div>{renderQuestion()}</div>
|
||||
{/* Navigasi Next dan Previous */}
|
||||
<div>
|
||||
<button onClick={prevQuestion} disabled={currentQuestionIndex === 0}>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={nextQuestion}
|
||||
disabled={currentQuestionIndex === questions.length - 1}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Exercise;
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
// import React, { useState, useEffect } from 'react';
|
||||
|
||||
// const MatchingPairsQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
// const [pairs, setPairs] = useState([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const initialPairs = question.matchingPairs.map((pair) => ({
|
||||
// left: pair.LEFT_PAIR,
|
||||
// right: '',
|
||||
// }));
|
||||
// if (savedAnswer) {
|
||||
// setPairs(savedAnswer);
|
||||
// } else {
|
||||
// setPairs(initialPairs);
|
||||
// }
|
||||
// }, [question, savedAnswer]);
|
||||
|
||||
// const rightOptions = question.matchingPairs.map((pair) => pair.RIGHT_PAIR);
|
||||
|
||||
// const handleSelect = (leftIndex, value) => {
|
||||
// const newPairs = [...pairs];
|
||||
// newPairs[leftIndex].right = value;
|
||||
// setPairs(newPairs);
|
||||
|
||||
// const isComplete = newPairs.every((pair) => pair.right !== '');
|
||||
// if (isComplete) {
|
||||
// onAnswer(index , newPairs);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <h3>{question.TITLE}</h3>
|
||||
// <p>{question.QUESTION}</p>
|
||||
// {question.VIDEO && (
|
||||
// <video controls>
|
||||
// <source src={question.VIDEO} type="video/mp4" />
|
||||
// </video>
|
||||
// )}
|
||||
// {question.IMAGE && <img src={question.IMAGE} alt="Question" />}
|
||||
// {question.AUDIO && <audio controls src={question.AUDIO}></audio>}
|
||||
// <div>
|
||||
// {pairs.map((pair, index) => (
|
||||
// <div key={index} style={{ display: 'flex', marginBottom: '10px' }}>
|
||||
// <div style={{ marginRight: '20px' }}>{pair.left}</div>
|
||||
// <select
|
||||
// value={pair.right}
|
||||
// onChange={(e) => handleSelect(index, e.target.value)}
|
||||
// >
|
||||
// <option value="">Pilih pasangan</option>
|
||||
// {rightOptions.map((option, idx) => (
|
||||
// <option key={idx} value={option}>
|
||||
// {option}
|
||||
// </option>
|
||||
// ))}
|
||||
// </select>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default MatchingPairsQuestion;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// components/MatchingPairsQuestion.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const colors = ['#E9342D', '#FACC15', '#1FBC2F', '#0090FF', '#ED27D9'];
|
||||
|
||||
const shuffleArray = (array) => {
|
||||
return array
|
||||
.map((value) => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value);
|
||||
};
|
||||
|
||||
function arrayToString(arr) {
|
||||
return arr.join(', ');
|
||||
}
|
||||
|
||||
function stringToArray(str) {
|
||||
return str.split(', ');
|
||||
}
|
||||
|
||||
const MatchingPairsQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const [pairs, setPairs] = useState([]);
|
||||
const [selectedLeft, setSelectedLeft] = useState(null);
|
||||
const [selectedRight, setSelectedRight] = useState(null);
|
||||
const [rightOptions, setRightOptions] = useState([]);
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
const [isShuffled, setIsShuffled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const initialPairs = question.matchingPairs.map((pair) => ({
|
||||
left: pair.LEFT_PAIR,
|
||||
right: '',
|
||||
color: colors[question.matchingPairs.indexOf(pair) % colors.length],
|
||||
}));
|
||||
|
||||
if (savedAnswer) {
|
||||
const arrSavedAnswer = stringToArray(savedAnswer);
|
||||
const updatedPairs = initialPairs.map((pair, index) => ({
|
||||
...pair,
|
||||
right: arrSavedAnswer[index],
|
||||
}));
|
||||
setPairs(updatedPairs);
|
||||
} else {
|
||||
setPairs(initialPairs);
|
||||
}
|
||||
|
||||
if (!isShuffled) {
|
||||
setIsShuffled(true);
|
||||
setRightOptions(shuffleArray(question.matchingPairs.map((pair) => pair.RIGHT_PAIR)));
|
||||
}
|
||||
}, [question, savedAnswer]);
|
||||
|
||||
const handleLeftClick = (index) => {
|
||||
setSelectedLeft(index);
|
||||
const status = pairs.findIndex(item => item.right === rightOptions[selectedRight]);
|
||||
if (selectedRight !== null) {
|
||||
makePair(index, selectedRight, status);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (index) => {
|
||||
setSelectedRight(index);
|
||||
const status = pairs.findIndex(item => item.right === rightOptions[index]);
|
||||
if (selectedLeft !== null) {
|
||||
makePair(selectedLeft, index, status);
|
||||
}
|
||||
};
|
||||
|
||||
const makePair = (leftIndex, rightIndex, changePair) => {
|
||||
const newPairs = [...pairs];
|
||||
if (changePair > -1) {
|
||||
setIsComplete(false);
|
||||
pairs[changePair].right = '';
|
||||
}
|
||||
|
||||
const selectedRightValue = rightOptions[rightIndex];
|
||||
newPairs[leftIndex].right = selectedRightValue;
|
||||
setPairs(newPairs);
|
||||
// console.log(newPairs);
|
||||
|
||||
setSelectedLeft(null);
|
||||
setSelectedRight(null);
|
||||
|
||||
const allPairsMatched = newPairs.every((pair) => pair.right !== '');
|
||||
if (allPairsMatched && !isComplete) {
|
||||
setIsComplete(true);
|
||||
|
||||
const rightAnswers = newPairs.map((pair) => pair.right);
|
||||
console.log(rightAnswers);
|
||||
onAnswer(index, arrayToString(rightAnswers));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{question.TITLE}</h3>
|
||||
<p>{question.QUESTION}</p>
|
||||
{question.VIDEO && (
|
||||
<video controls>
|
||||
<source src={question.VIDEO} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
{question.IMAGE && <img src={question.IMAGE} alt="Question" />}
|
||||
{question.AUDIO && <audio controls src={question.AUDIO}></audio>}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '20px' }}>
|
||||
{/* Bagian kiri */}
|
||||
<div>
|
||||
{pairs.map((pair, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleLeftClick(index)}
|
||||
style={{
|
||||
padding: '10px',
|
||||
margin: '10px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedLeft === index ? '#ccc' : pair.color,
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{pair.left}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bagian kanan */}
|
||||
<div>
|
||||
{rightOptions.map((right, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleRightClick(index)}
|
||||
style={{
|
||||
padding: '10px',
|
||||
margin: '10px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor:
|
||||
pairs.find((pair) => pair.right === right)?.color ||
|
||||
(selectedRight === index ? '#ccc' : '#f0f0f0'),
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{right}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchingPairsQuestion;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const MultipleChoiceQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const options = question.multipleChoices[0];
|
||||
|
||||
const handleChange = (e) => {
|
||||
onAnswer(index, e.target.value);
|
||||
};
|
||||
|
||||
function getOptionLetter(option) {
|
||||
const match = option.match(/OPTION_(\w)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{question.TITLE}</h3>
|
||||
<p>{question.QUESTION}</p>
|
||||
{question.VIDEO && (
|
||||
<video controls>
|
||||
<source src={question.VIDEO} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
{question.IMAGE && <img src={question.IMAGE} alt="Question" />}
|
||||
{question.AUDIO && <audio controls src={question.AUDIO}></audio>}
|
||||
<div>
|
||||
{['OPTION_A', 'OPTION_B', 'OPTION_C', 'OPTION_D', 'OPTION_E'].map(
|
||||
(optionKey) => {
|
||||
if (options[optionKey]) {
|
||||
return (
|
||||
<div key={optionKey}>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name={`question-${question.ID_ADMIN_EXERCISE}`}
|
||||
value={getOptionLetter(optionKey)}
|
||||
onChange={handleChange}
|
||||
checked={savedAnswer === getOptionLetter(optionKey)}
|
||||
/>
|
||||
{options[optionKey]}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultipleChoiceQuestion;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const TrueFalseQuestion = ({ question, onAnswer, savedAnswer, index }) => {
|
||||
const handleChange = (e) => {
|
||||
onAnswer(index, e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{question.TITLE}</h3>
|
||||
<p>{question.QUESTION}</p>
|
||||
{question.VIDEO && (
|
||||
<video controls>
|
||||
<source src={question.VIDEO} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
{question.IMAGE && <img src={question.IMAGE} alt="Question" />}
|
||||
{question.AUDIO && <audio controls src={question.AUDIO}></audio>}
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name={`question-${question.ID_ADMIN_EXERCISE}`}
|
||||
value="1"
|
||||
onChange={handleChange}
|
||||
checked={savedAnswer === '1'}
|
||||
/>
|
||||
True
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name={`question-${question.ID_ADMIN_EXERCISE}`}
|
||||
value="0"
|
||||
onChange={handleChange}
|
||||
checked={savedAnswer === '0'}
|
||||
/>
|
||||
False
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrueFalseQuestion;
|
||||
|
|
@ -57,13 +57,12 @@ const useLevels = (section, topic) => {
|
|||
fetchData();
|
||||
}, [section, topic]);
|
||||
|
||||
const isLevelUnlocked = (levelId) => {
|
||||
if (levelId !== 0) {
|
||||
if (!lastCompletedLevel){
|
||||
return false;
|
||||
const isLevelUnlocked = (levelIndex) => {
|
||||
if (levelIndex !== 0) {
|
||||
if (levels[levelIndex].CONTENT) {
|
||||
return true;
|
||||
}else{
|
||||
const maxUnlock = extractLevelNumber(lastCompletedLevel.NAME_LEVEL);
|
||||
return levelId <= maxUnlock;
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -10,15 +10,6 @@ import Skeleton from 'react-loading-skeleton';
|
|||
|
||||
const Level = () => {
|
||||
const { section, topic } = useParams();
|
||||
// const { isValidSection, isValidTopic } = useValidation();
|
||||
|
||||
// if (!isValidSection(section)) {
|
||||
// return <Navigate to="/not-found" />;
|
||||
// } else {
|
||||
// if (!isValidTopic(topic)) {
|
||||
// return <Navigate to="/not-found" />;
|
||||
// }
|
||||
// }
|
||||
|
||||
const { levels, lastCompletedLevel, loading, error, isLevelUnlocked, levelDesc } = useLevels(section, topic);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user