add user review

This commit is contained in:
Dimas Atmodjo 2024-12-05 15:30:22 +07:00
parent d2bb26e644
commit b661043755
11 changed files with 869 additions and 8 deletions

View File

@ -42,7 +42,7 @@
height: 30px; height: 30px;
position: absolute; position: absolute;
top: calc(50% - 30px); top: calc(50% - 30px);
left: calc(100% - 15px); left: calc(100% - 12px);
padding: 0; padding: 0;
} }
@ -110,6 +110,18 @@
object-fit: cover; object-fit: cover;
} }
.user-main-layout{
height: calc(100vh - 58px);
overflow-y: auto;
}
.exercise-history .nav.nav-pills .nav-item a, .exercise-history .nav.nav-pills .nav-item a,
.topic-page .nav.nav-pills .nav-item a, .topic-page .nav.nav-pills .nav-item a,
.setting-page .nav.nav-pills .nav-item a{ .setting-page .nav.nav-pills .nav-item a{
@ -228,6 +240,53 @@
color: #0090FF; color: #0090FF;
} }
.exercise-page .review-list .number-label{
color: #959EA9;
display: flex;
}
.exercise-page .review-list .number-label:not(.active):hover{
background-color: #00BC650c;
}
.exercise-page .review-list .number-label.correct.active{
color: #ffffff;
background-color: #00BC65;
}
.exercise-page .review-list .number-label.incorrect.active{
color: #ffffff;
background-color: #E9342D;
}
.exercise-page .review-list .number-label.correct:not(.active){
color: #00BC65;
}
.exercise-page .review-list .number-label.incorrect:not(.active){
color: #E9342D;
}
.exercise-page .correction-label{
width: 55%;
display: flex;
align-items: center;
color: #ffffff;
margin-bottom: 12px;
padding: 8px 16px;
border-radius: 8px;
}
.exercise-page .correction-label.correct{
background-color: #00BC65;
}
.exercise-page .correction-label.incorrect{
background-color: #E9342D;
}
.exercise-page .options .form-check, .exercise-page .options .form-check,
.exercise-page .options-tf .form-check, .exercise-page .options-tf .form-check,
.exercise-page .options-mp .form-check { .exercise-page .options-mp .form-check {

View File

@ -56,7 +56,7 @@ const UserLayout = ({ children }) => {
<div className="row min-h-100"> <div className="row min-h-100">
{/* <SideNav /> */} {/* <SideNav /> */}
{getSideNav() ? <SideNav /> : ""} {getSideNav() ? <SideNav /> : ""}
<main className="col p-4 overflow-auto bg-light"> <main className="col p-4 overflow-auto bg-light user-main-layout">
{children} {children}
</main> </main>
</div> </div>

View File

@ -6,6 +6,8 @@ import logoW from '../../../assets/images/logo-w.png';
import logoutIllustration from '../../../assets/images/illustration/logout.png'; import logoutIllustration from '../../../assets/images/illustration/logout.png';
import useAuth from '../../../roles/guest/auth/hooks/useAuth'; import useAuth from '../../../roles/guest/auth/hooks/useAuth';
import { headerSection, headerTopic, headerLevel } from '../../../utils/Constant';
import Report from '../../../roles/user/report/views/Report'; import Report from '../../../roles/user/report/views/Report';
import { unSlugify } from '../../../utils/Constant'; import { unSlugify } from '../../../utils/Constant';
@ -68,15 +70,22 @@ const UserNavbar = () => {
</Button> </Button>
<div className="d-none d-md-block navbar-title col-md-6 col-lg-8"> <div className="d-none d-md-block navbar-title col-md-6 col-lg-8">
{hasTopicAndLevel ? ( {hasTopicAndLevel ? (
lastSegment === 'exercise' ? ( lastSegment === 'review' ? (
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}> <h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
{`${unSlugify(pathSegments[3])} : ${unSlugify(pathSegments[4])} `} {headerSection} : {headerTopic}
<span className='text-dark'> - {unSlugify(pathSegments[5])}</span> <span className='text-dark'> - {headerLevel}</span>
</h5> </h5>
):( ):(
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}> lastSegment === 'exercise' ? (
{unSlugify(pathSegments[4])} <h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
</h5> {`${unSlugify(pathSegments[3])} : ${unSlugify(pathSegments[4])} `}
<span className='text-dark'> - {unSlugify(pathSegments[5])}</span>
</h5>
):(
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
{unSlugify(pathSegments[4])}
</h5>
)
) )
):('')} ):('')}
</div> </div>

View File

@ -0,0 +1,66 @@
import { useState, useEffect } from 'react';
import exerciseService from '../services/reviewService';
export const useReview = (stdLearning) => {
const [questions, setQuestions] = useState([]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
const response = await exerciseService.fetchReview(stdLearning);
setQuestions(response.payload.stdExercises);
} catch (error) {
console.error("something wrong : ", error);
setError(error);
}finally{
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, [stdLearning]);
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]);
return {
questions,
currentQuestion: questions[currentQuestionIndex],
currentQuestionIndex,
setCurrentQuestionIndex,
answers,
isLoading,
error,
nextQuestion,
prevQuestion,
};
};

View File

@ -0,0 +1,81 @@
import axiosInstance from '../../../../utils/axiosInstance';
const getSoalNumber = (title) => {
const match = title.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
};
const getLevelId = async (topicId, levelName) => {
try {
const response = await axiosInstance.get(`/level/topic/${topicId}`);
const filteredData = response.data.data.levels.filter(item => item.NAME_LEVEL === levelName);
return filteredData[0].ID_LEVEL;
} catch (error) {
return [];
}
};
const createStdLearning = async (data) => {
try {
const response = await axiosInstance.post(`/stdLearning`, data);
return response.data;
} catch (error) {
console.error('Error creating std_learning:', error);
throw error;
}
};
const fetchReview = async (stdLearning) => {
try {
const response = await axiosInstance.get(`/studentAnswers/${stdLearning}`);
return response.data;
} catch (error) {
throw error;
}
};
const sumbitAnswer = async (dataAnswer) => {
try {
const response = await axiosInstance.post(`/stdExercise`, dataAnswer);
return response.data;
} catch (error) {
console.error('Error submit exercise:', error);
throw error;
}
};
const checkStdLearning = async (level) => {
try {
const response = await axiosInstance.get(`/stdLearning/level/${level}`);
return response.data.payload.ID_STUDENT_LEARNING;
} catch (error) {
// console.error('Error checking std_learning:', error);
// throw error;
return null;
}
};
const getScore = async (stdLearning) => {
try {
const response = await axiosInstance.get(`/stdLearning/score/${stdLearning}`);
return response.data;
} catch (error) {
console.error('Error fetching result:', error);
throw error;
}
};
export default {
getLevelId,
checkStdLearning,
createStdLearning,
fetchReview,
sumbitAnswer,
getScore
};

View File

@ -0,0 +1,148 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useReview } from '../hooks/useReview';
import { Container, Row, Col, ListGroup, Button, 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';
const Review = () => {
const { stdLearning } = useParams();
const {
questions,
currentQuestion,
currentQuestionIndex,
setCurrentQuestionIndex,
answers,
isLoading,
error,
nextQuestion,
prevQuestion,
} = useReview(stdLearning);
const renderQuestion = () => {
switch (currentQuestion.QUESTION_TYPE) {
case 'MCQ':
return (
<MultipleChoiceQuestion
question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT}
index={currentQuestionIndex}
/>
);
case 'TFQ':
return (
<TrueFalseQuestion
question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT}
index={currentQuestionIndex}
/>
);
case 'MPQ':
return (
<MatchingPairsQuestion
question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT}
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 <h1 className='text-center'>Exercise questions not yet available</h1>;
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'>Review</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='review-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 fw-bold ${q.IS_CORRECT === 1 ? 'correct' : 'incorrect'}`}
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>
</div>
<div className='p-3 border rounded-3'>
{renderQuestion()}
</div>
</div>
</Col>
</Row>
</Container>
);
};
export default Review;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { MEDIA_URL } from '../../../../../utils/Constant';
const ExerciseMedia = ({ image, audio, video }) => {
const mediaPath = `${MEDIA_URL}/exercise`;
return (
<div>
{image !== null && (
<div className='my-1'>
<img src={`${mediaPath}/image/${image}`} alt="" />
{/* <h3>{`${MEDIA_URL}/exercise/image/${image}`}</h3> */}
</div>
)}
{audio !== null && (
<div className='my-1'>
{/* <audio controls>
<source src={audio} type="audio/mpeg" />
Your browser does not support the audio element.
</audio> */}
<h3>{`${mediaPath}/audio/${audio}`}</h3>
</div>
)}
{video !== null && (
// <video src={video} controls className='my-1'></video>
<div className="my-1">
<h3>{video}</h3>
</div>
)}
</div>
);
};
export default ExerciseMedia;

View File

@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import MediaViewer from './MediaViewer';
import { MEDIA_URL } from '../../../../../utils/Constant';
// 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, studentAnswer, index }) => {
const savedAnswer = studentAnswer !== null ? studentAnswer : null;
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 !== null) {
const arrSavedAnswer = stringToArray(savedAnswer);
const updatedPairs = initialPairs.map((pair, index) => ({
...pair,
right: arrSavedAnswer[index],
}));
setPairs(updatedPairs);
} else {
setPairs(initialPairs);
}
}, [question, savedAnswer]);
useEffect(() => {
setIsShuffled(true);
setRightOptions(shuffleArray(question.matchingPairs.map((pair) => pair.RIGHT_PAIR)));
}, [question]);
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);
onAnswer(index, arrayToString(rightAnswers), question.ID_ADMIN_EXERCISE);
}
};
const mediaUrls = [];
const mediaPath = `${MEDIA_URL}/exercise`;
if (question.IMAGE) mediaUrls.push(`${mediaPath}/image/${question.IMAGE}`);
if (question.AUDIO) mediaUrls.push(`${mediaPath}/audio/${question.AUDIO}`);
if (question.VIDEO) mediaUrls.push(question.VIDEO);
return (
<div>
<div className={`correction-label ${question.IS_CORRECT === 1 ? `correct` : `incorrect` }`}>
<i className={`me-2 bi ${question.IS_CORRECT === 1 ? `bi-check-circle-fill` : `bi-x-circle-fill` }`}></i>
Your matching pairs answers are {question.RESULT_SCORE_STUDENT}/{question.SCORE_WEIGHT} correct.
</div>
{mediaUrls.length > 0 && <MediaViewer mediaUrls={mediaUrls} />}
<p>{question.QUESTION.split('\n').map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
))}</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`}
style={{
display: 'flex', alignItems: 'center',
}}
>
<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`}
style={{
display: 'flex', alignItems: 'center',
}}
>
<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;

View File

@ -0,0 +1,122 @@
import React, { useState, useEffect } from 'react';
import Skeleton from 'react-loading-skeleton';
const MediaViewer = ({ mediaUrls }) => {
const [loadedMedia, setLoadedMedia] = useState([]);
const [loading, setLoading] = useState(true);
function isMediaUrl(url) {
const urls = url[0];
const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'];
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'];
const lowercasedUrl = urls.toLowerCase();
const isAudio = audioExtensions.some(extension => lowercasedUrl.endsWith(extension));
const isImage = imageExtensions.some(extension => lowercasedUrl.endsWith(extension));
return isAudio || isImage;
}
const preloadMedia = (urls) => {
return Promise.all(
urls.map((url) => {
return new Promise((resolve, reject) => {
const mediaType = url.split('.').pop();
if (['jpg', 'jpeg', 'png', 'gif'].includes(mediaType)) {
const img = new Image();
img.src = url;
img.onload = () => resolve({ type: 'image', url });
img.onerror = () => reject(`Failed to load image: ${url}`);
} else if (['mp3', 'wav'].includes(mediaType)) {
const audio = new Audio();
audio.src = url;
audio.onloadedmetadata = () => resolve({ type: 'audio', url });
audio.onerror = () => reject(`Failed to load audio: ${url}`);
} else {
resolve({ type: 'unknown', url });
}
});
})
);
};
const renderVideo = (url) => {
// Cek apakah ini link YouTube
if (url.includes('youtube.com') || url.includes('youtu.be')) {
const youtubeId = url.includes('youtube.com')
? new URLSearchParams(new URL(url).search).get('v') // Ambil ID dari link YouTube
: url.split('/').pop(); // Ambil ID dari youtu.be link
return (
<iframe
src={`https://www.youtube.com/embed/${youtubeId}`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="YouTube Video"
className='video-box'
></iframe>
);
} else if (url.includes('drive.google.com')) {
const driveId = url.includes('file/d/') ? url.split('file/d/')[1].split('/')[0] : '';
return (
<iframe
src={`https://drive.google.com/file/d/${driveId}/preview`}
allow="autoplay"
title="Google Drive Video"
className='video-box'
></iframe>
);
} else {
return (
<video controls className='video-box'>
<source src={url} type="video/mp4" />
Your browser does not support the video tag.
</video>
);
}
};
useEffect(() => {
if (mediaUrls && mediaUrls.length > 0 && isMediaUrl(mediaUrls)) {
preloadMedia(mediaUrls)
.then((loaded) => {
setLoadedMedia(loaded);
setLoading(false);
})
.catch((error) => {
console.error(error);
setLoading(false);
});
} else {
setLoading(false);
}
}, [mediaUrls]);
if (loading) {
return (
<Skeleton containerClassName='w-50 d-block' className='w-100 mb-1 rounded-3' style={{height:"20vh"}} />
);
}
return (
<div className='my-1'>
{isMediaUrl(mediaUrls) ? (
loadedMedia.map((media, index) => {
if (media.type === 'image') {
return <img key={index} src={media.url} alt={`Media ${index}`} />;
} else if (media.type === 'audio') {
return <audio key={index} controls src={media.url}></audio>;
} else {
return <div key={index}>Unknown media type</div>;
}
})
) : (
renderVideo(mediaUrls[0]) // Panggil fungsi renderVideo dengan URL yang benar
)}
</div>
);
};
export default MediaViewer;

View File

@ -0,0 +1,61 @@
import React from 'react';
import MediaViewer from './MediaViewer';
import { MEDIA_URL } from '../../../../../utils/Constant';
const MultipleChoiceQuestion = ({ question, studentAnswer, index }) => {
const savedAnswer = studentAnswer !== null ? studentAnswer : null;
const options = question.multipleChoices[0];
const mediaUrls = [];
const mediaPath = `${MEDIA_URL}/exercise`;
if (question.IMAGE) mediaUrls.push(`${mediaPath}/image/${question.IMAGE}`);
if (question.AUDIO) mediaUrls.push(`${mediaPath}/audio/${question.AUDIO}`);
if (question.VIDEO) mediaUrls.push(question.VIDEO);
function getOptionLetter(option) {
const match = option.match(/OPTION_(\w)/);
return match ? match[1] : null;
}
return (
<div>
<div className={`correction-label ${question.IS_CORRECT === 1 ? `correct` : `incorrect` }`}>
<i className={`me-2 bi ${question.IS_CORRECT === 1 ? `bi-check-circle-fill` : `bi-x-circle-fill` }`}></i>
Your answer is {question.IS_CORRECT === 1 ? `correct` : `incorrect` }.
</div>
{mediaUrls.length > 0 && <MediaViewer mediaUrls={mediaUrls} />}
<p>{question.QUESTION.split('\n').map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
))}</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'}}
>
<span className="option-label">{getOptionLetter(optionKey)}</span>
<span className="ms-2 option-text">{options[optionKey]}</span>
</div>
);
}
return null;
}
)}
</div>
</div>
);
};
export default MultipleChoiceQuestion;

View File

@ -0,0 +1,54 @@
import React from 'react';
import MediaViewer from './MediaViewer';
import { MEDIA_URL } from '../../../../../utils/Constant';
const TrueFalseQuestion = ({ question, studentAnswer, index }) => {
const savedAnswer = studentAnswer !== null ? studentAnswer : null;
const mediaUrls = [];
const mediaPath = `${MEDIA_URL}/exercise`;
if (question.IMAGE) mediaUrls.push(`${mediaPath}/image/${question.IMAGE}`);
if (question.AUDIO) mediaUrls.push(`${mediaPath}/audio/${question.AUDIO}`);
if (question.VIDEO) mediaUrls.push(question.VIDEO);
return (
<div>
<div className={`correction-label ${question.IS_CORRECT === 1 ? `correct` : `incorrect` }`}>
<i className={`me-2 bi ${question.IS_CORRECT === 1 ? `bi-check-circle-fill` : `bi-x-circle-fill` }`}></i>
Your answer is {question.IS_CORRECT === 1 ? `correct` : `incorrect` }.
</div>
{mediaUrls.length > 0 && <MediaViewer mediaUrls={mediaUrls} />}
<p>{question.QUESTION.split('\n').map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
))}</p>
<div className="options-tf">
<div
className={`p-0 mb-3 form-check ${savedAnswer === '1' ? 'selected-answer' : ''}`}
style={{ display: 'flex', alignItems: 'center'}}
>
<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'}}
>
<span className="option-label">B</span>
<span className="ms-2 option-text">FALSE</span>
</div>
</div>
</div>
);
};
export default TrueFalseQuestion;