fix(refactor): Level page review pretest navigator and Refactor history and review services for improved error handling and code consistency

This commit is contained in:
Resh 2024-12-11 10:44:47 +07:00
parent 9a3c1f678d
commit f44fe0a9b9
5 changed files with 686 additions and 494 deletions

View File

@ -1,54 +1,75 @@
import axiosInstance from '../../../../utils/axiosInstance'; import axiosInstance from '../../../../utils/axiosInstance'
const fetchTopic = async () => { const fetchTopic = async () => {
try { try {
const response = await axiosInstance.get(`/topic`); const response = await axiosInstance.get(`/topic`)
return response.data; return response.data
} catch (error) { } catch (error) {
throw error; console.error(error)
} throw error
}; }
}
const fetchAllHistory = async () => { const fetchAllHistory = async () => {
try { try {
const response = await axiosInstance.get(`/learningHistory`); const response = await axiosInstance.get(`/learningHistory`)
const historyData = response.data.payload.history; const historyData = response.data.payload.history
const validHistory = historyData.filter(history => history.STUDENT_FINISH !== null); const validHistory = historyData.filter(
history => history.STUDENT_FINISH !== null
return {message: response.data.message, payload: validHistory, status: response.data.statusCode,}; )
} catch (error) { return {
throw error; message: response.data.message,
} payload: validHistory,
}; status: response.data.statusCode,
}
} catch (error) {
console.error(error)
throw error
}
}
const fetchHistoryBySection = async (section) => { const fetchHistoryBySection = async section => {
try { try {
const response = await axiosInstance.get(`/learningHistory/section/${section}`); const response = await axiosInstance.get(
const historyData = response.data.payload.history; `/learningHistory/section/${section}`
const validHistory = historyData.filter(history => history.STUDENT_FINISH !== null); )
const historyData = response.data.payload.history
const validHistory = historyData.filter(
history => history.STUDENT_FINISH !== null
)
return {message: response.data.message, payload: validHistory, status: response.data.statusCode}; return {
} catch (error) { message: response.data.message,
return {payload: []}; payload: validHistory,
} status: response.data.statusCode,
}; }
} catch (error) {
return { payload: [] }
}
}
const fetchHistoryByTopic = async (topic) => { const fetchHistoryByTopic = async topic => {
try { try {
const response = await axiosInstance.get(`/learningHistory/topic/${topic}`); const response = await axiosInstance.get(`/learningHistory/topic/${topic}`)
const historyData = response.data.payload.history; const historyData = response.data.payload.history
const validHistory = historyData.filter(history => history.STUDENT_FINISH !== null); const validHistory = historyData.filter(
history => history.STUDENT_FINISH !== null
return {message: response.data.message, payload: validHistory, status: response.data.statusCode,}; )
} catch (error) {
return {payload: []}; return {
} message: response.data.message,
}; payload: validHistory,
status: response.data.statusCode,
}
} catch (error) {
return { payload: [] }
}
}
export default { export default {
fetchTopic, fetchTopic,
fetchAllHistory, fetchAllHistory,
fetchHistoryBySection, fetchHistoryBySection,
fetchHistoryByTopic fetchHistoryByTopic,
}; }

View File

@ -1,90 +1,148 @@
import React from 'react'; /* eslint-disable no-mixed-spaces-and-tabs */
import { Container, Row, Col, Card, Button, Dropdown, Breadcrumb, Tab, Nav } from 'react-bootstrap'; import {
import useHistories from '../hooks/useHirtories'; Container,
import { Link } from 'react-router-dom'; Row,
import newBie from '../../../../assets/images/illustration/emptyJourney.png'; Col,
import Skeleton from 'react-loading-skeleton'; Card,
Button,
Dropdown,
Breadcrumb,
Tab,
Nav,
} from 'react-bootstrap'
import useHistories from '../hooks/useHirtories'
import { Link } from 'react-router-dom'
import newBie from '../../../../assets/images/illustration/emptyJourney.png'
import Skeleton from 'react-loading-skeleton'
const ExerciseHistory = () => { const ExerciseHistory = () => {
const { const {
historyData, historyData,
sectionData, sectionData,
loading, loading,
error, error,
activeTab, activeTab,
topicsBySection, topicsBySection,
selectedTopic, selectedTopic,
setActiveTab, setActiveTab,
formatLocalDate, formatLocalDate,
compareLevels, compareLevels,
setSelectedTopic setSelectedTopic,
} = useHistories(); } = useHistories()
if (error) return <p>{error}</p>; if (error) return <p>{error}</p>
const topics = activeTab !== 'all' ? topicsBySection[activeTab] || []: []; const topics = activeTab !== 'all' ? topicsBySection[activeTab] || [] : []
return ( return (
<div className='exercise-history'> <div className="exercise-history">
<h3 className='fw-bold text-blue'>Your Exercise History!</h3> <h3 className="fw-bold text-blue">Your Exercise History!</h3>
<p className='text-secondary mb-4'>Track your progress with a personalized overview of all your workouts.</p> <p className="text-secondary mb-4">
<Container fluid className='mb-4 d-flex align-items-center bg-white rounded rounded-4'> Track your progress with a personalized overview of all your workouts.
<Tab.Container id="left-tabs-example" defaultActiveKey="all" onSelect={(k) => { setActiveTab(k); setSelectedTopic(''); }}> </p>
<Row> <Container
<Col> fluid
<Nav variant="pills" className="p-2 bg-white"> className="mb-4 d-flex align-items-center bg-white rounded rounded-4"
<Nav.Item> >
<Nav.Link eventKey="all">All</Nav.Link> <Tab.Container
</Nav.Item> id="left-tabs-example"
{sectionData.map((section, index) => ( defaultActiveKey="all"
<Nav.Item key={index}> onSelect={k => {
<Nav.Link eventKey={section}>{section}</Nav.Link> setActiveTab(k)
</Nav.Item> setSelectedTopic('')
))} }}
</Nav> >
</Col> <Row>
</Row> <Col>
</Tab.Container> <Nav variant="pills" className="p-2 bg-white">
{topics.length > 0 && ( <Nav.Item>
<Dropdown className='ms-auto' onSelect={(topicId) => setSelectedTopic(topicId)}> <Nav.Link eventKey="all">All</Nav.Link>
<Dropdown.Toggle id="dropdown-basic" size='sm' variant="outline-blue rounded-3" style={{padding:"6px 16px"}}> </Nav.Item>
{selectedTopic ? topics.find(t => t.ID_TOPIC === selectedTopic)?.NAME_TOPIC : 'Select a Topic'} {sectionData.map((section, index) => (
</Dropdown.Toggle> <Nav.Item key={index}>
<Dropdown.Menu> <Nav.Link eventKey={section}>{section}</Nav.Link>
{topics.map((topic, index) => ( </Nav.Item>
<Dropdown.Item key={topic.ID_TOPIC ? topic.ID_TOPIC : index} eventKey={topic.ID_TOPIC ? topic.ID_TOPIC : index}> ))}
{topic.NAME_TOPIC} </Nav>
</Dropdown.Item> </Col>
))} </Row>
</Dropdown.Menu> </Tab.Container>
</Dropdown> {topics.length > 0 && (
)} <Dropdown
</Container> className="ms-auto"
<Container fluid className='bg-white rounded rounded-4 p-4'> onSelect={topicId => setSelectedTopic(topicId)}
<Row xs={1}> >
{loading?( <Dropdown.Toggle
<Col> id="dropdown-basic"
<Card className='flex-column p-3 border rounded-4'> size="sm"
<div className="w-100 d-flex justify-content-between"> variant="outline-blue rounded-3"
<Skeleton containerClassName="mb-2" className='mb-1 rounded-4' style={{width:"15vw", height:"2vw"}} /> style={{ padding: '6px 16px' }}
<Skeleton containerClassName="mb-2" className='mb-1 rounded-4' style={{width:"25vw", height:"2vw"}} /> >
</div> {selectedTopic
<Skeleton containerClassName="mb-2" className='mb-1 rounded-4' style={{width:"40vw", height:"2vw"}} /> ? topics.find(t => t.ID_TOPIC === selectedTopic)?.NAME_TOPIC
<div className="w-100 d-flex justify-content-between"> : 'Select a Topic'}
<div className='d-flex'> </Dropdown.Toggle>
<Skeleton containerClassName="mb-2" className='mb-1 me-2 rounded-4' style={{width:"4vw", height:"5vw"}} /> <Dropdown.Menu>
<Skeleton containerClassName="mb-2" className='mb-1 rounded-4' style={{width:"15vw", height:"5vw"}} /> {topics.map((topic, index) => (
</div> <Dropdown.Item
<Skeleton containerClassName="mb-2" className='mb-1 rounded-4' style={{width:"30vw", height:"5vw"}} /> key={topic.ID_TOPIC ? topic.ID_TOPIC : index}
</div> eventKey={topic.ID_TOPIC ? topic.ID_TOPIC : index}
</Card> >
</Col> {topic.NAME_TOPIC}
) : </Dropdown.Item>
historyData.length > 0 ? ( ))}
historyData.map((history, index) => ( </Dropdown.Menu>
<Col key={index} className='mb-3'> </Dropdown>
<Card className='flex-row p-3 border-primary rounded-4'> )}
{/* <Card.Body className='p-0 d-flex flex-column justify-content-between'> </Container>
<Container fluid className="bg-white rounded rounded-4 p-4">
<Row xs={1}>
{loading ? (
<Col>
<Card className="flex-column p-3 border rounded-4">
<div className="w-100 d-flex justify-content-between">
<Skeleton
containerClassName="mb-2"
className="mb-1 rounded-4"
style={{ width: '15vw', height: '2vw' }}
/>
<Skeleton
containerClassName="mb-2"
className="mb-1 rounded-4"
style={{ width: '25vw', height: '2vw' }}
/>
</div>
<Skeleton
containerClassName="mb-2"
className="mb-1 rounded-4"
style={{ width: '40vw', height: '2vw' }}
/>
<div className="w-100 d-flex justify-content-between">
<div className="d-flex">
<Skeleton
containerClassName="mb-2"
className="mb-1 me-2 rounded-4"
style={{ width: '4vw', height: '5vw' }}
/>
<Skeleton
containerClassName="mb-2"
className="mb-1 rounded-4"
style={{ width: '15vw', height: '5vw' }}
/>
</div>
<Skeleton
containerClassName="mb-2"
className="mb-1 rounded-4"
style={{ width: '30vw', height: '5vw' }}
/>
</div>
</Card>
</Col>
) : historyData.length > 0 ? (
historyData.map((history, index) => (
<Col key={index} className="mb-3">
<Card className="flex-row p-3 border-primary rounded-4">
{/* <Card.Body className='p-0 d-flex flex-column justify-content-between'>
<Card.Title className='mb-1 w-100 d-flex justify-content-between'> <Card.Title className='mb-1 w-100 d-flex justify-content-between'>
<h4 className='mb-0'>{history.CURRENT_LEVEL}</h4> <h4 className='mb-0'>{history.CURRENT_LEVEL}</h4>
</Card.Title> </Card.Title>
@ -122,64 +180,85 @@ const ExerciseHistory = () => {
</div> </div>
</div> </div>
</Card.Body> */} </Card.Body> */}
<Card.Body className="p-2 d-flex justify-content-between"> <Card.Body className="p-2 d-flex justify-content-between">
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<h4 className='mb-0'>{history.CURRENT_LEVEL}</h4> <h4 className="mb-0">{history.CURRENT_LEVEL}</h4>
<Breadcrumb className='mt-2 custom-breadcrumb'> <Breadcrumb className="mt-2 custom-breadcrumb">
<Breadcrumb.Item>{history.SECTION_NAME}</Breadcrumb.Item> <Breadcrumb.Item>
<Breadcrumb.Item active>{history.TOPIC_NAME}</Breadcrumb.Item> {history.SECTION_NAME}
</Breadcrumb> </Breadcrumb.Item>
<p className='mt-2 mb-0 submit-time'>Submission: {formatLocalDate(history.STUDENT_FINISH)}</p> <Breadcrumb.Item active>
<h3 className='mt-4 mb-0'> {history.TOPIC_NAME}
<i className="bi bi-trophy-fill text-warning"></i> </Breadcrumb.Item>
<span className='ms-3 text-blue fw-bold'>{history.SCORE}/100</span> </Breadcrumb>
</h3> <p className="mt-2 mb-0 submit-time">
</div> Submission: {formatLocalDate(history.STUDENT_FINISH)}
<div className="d-flex flex-column justify-content-between align-items-end"> </p>
{history.IS_PASS == 1?( <h3 className="mt-4 mb-0">
<h5 className='text-green'> <i className="bi bi-trophy-fill text-warning"></i>
<i className="bi bi-check2-all me-1"></i> <span className="ms-3 text-blue fw-bold">
Topic Finished! {history.SCORE}/100
</h5> </span>
):( </h3>
compareLevels(history.CURRENT_LEVEL, history.NEXT_LEVEL) === 'jump' ?( </div>
<h5 className='text-blue'> <div className="d-flex flex-column justify-content-between align-items-end">
<i className="bi bi-arrow-up me-1"></i> {history.IS_PASS == 1 ? (
Jump to {history.NEXT_LEVEL} <h5 className="text-green">
</h5> <i className="bi bi-check2-all me-1"></i>
) : ( Topic Finished!
compareLevels(history.CURRENT_LEVEL, history.NEXT_LEVEL) === 'down' ?( </h5>
<h5 className='text-red'> ) : compareLevels(
<i className="bi bi-arrow-down me-1"></i> history.CURRENT_LEVEL,
Go Down to {history.NEXT_LEVEL} history.NEXT_LEVEL
</h5> ) === 'jump' ? (
) : ( <h5 className="text-blue">
<h5 className='text-dark'> <i className="bi bi-arrow-up me-1"></i>
<i className="bi bi-arrow-repeat me-1"></i> Jump to {history.NEXT_LEVEL}
Stay in {history.NEXT_LEVEL} </h5>
</h5> ) : compareLevels(
) history.CURRENT_LEVEL,
) history.NEXT_LEVEL
)} ) === 'down' ? (
<Link to={`/learning/review/${history.ID_STUDENT_LEARNING}`} className='btn btn-blue py-2 px-5 rounded-35'>Review</Link> <h5 className="text-red">
</div> <i className="bi bi-arrow-down me-1"></i>
</Card.Body> Go Down to {history.NEXT_LEVEL}
</Card> </h5>
</Col> ) : (
)) <h5 className="text-dark">
) : ( <i className="bi bi-arrow-repeat me-1"></i>
<Col className='d-flex flex-column items-center'> Stay in {history.NEXT_LEVEL}
<h4 className='mb-0'>Still new?</h4> </h5>
<p className='fs-5 text-muted fw-light'>Begin your journey!</p> )}
<img src={newBie} alt="" /> <Link
<Button as={Link} to={'/learning/module'} variant='warning' className='mt-4 py-2 px-5 rounded-35'>Explore</Button> to={`/learning/review/${history.ID_STUDENT_LEARNING}`}
</Col> className="btn btn-blue py-2 px-5 rounded-35"
) >
} Review
</Row> </Link>
</Container> </div>
</div> </Card.Body>
); </Card>
}; </Col>
))
) : (
<Col className="d-flex flex-column items-center">
<h4 className="mb-0">Still new?</h4>
<p className="fs-5 text-muted fw-light">Begin your journey!</p>
<img src={newBie} alt="" />
<Button
as={Link}
to={'/learning/module'}
variant="warning"
className="mt-4 py-2 px-5 rounded-35"
>
Explore
</Button>
</Col>
)}
</Row>
</Container>
</div>
)
}
export default ExerciseHistory; export default ExerciseHistory

View File

@ -1,110 +1,157 @@
import React from 'react'; import { Container, Row, Col, Button, Breadcrumb } from 'react-bootstrap'
import { Container, Row, Col, Button, Breadcrumb } from 'react-bootstrap'; import { Link, useParams } from 'react-router-dom'
import { Link, useParams, Navigate } from 'react-router-dom'; import check from '../../../../assets/images/illustration/checkIcon.png'
import check from '../../../../assets/images/illustration/checkIcon.png';
// import { useValidation } from '../../utils/ValidationContext'; // import { useValidation } from '../../utils/ValidationContext';
import useLevels from '../hooks/useLevels'; import useLevels from '../hooks/useLevels'
import { unSlugify } from '../../../../utils/Constant'; import { unSlugify } from '../../../../utils/Constant'
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton'
const Level = () => { const Level = () => {
const { section, topic } = useParams(); const { section, topic } = useParams()
const { levels, lastCompletedLevel, loading, error, isLevelUnlocked, levelDesc } = useLevels(section, topic); const { levels, loading, error, isLevelUnlocked, levelDesc } = useLevels(
section,
topic
)
if (loading) { if (loading) {
return ( return (
<div className='row'> <div className="row">
<Skeleton containerClassName='w-100' className='w-100 mb-3 rounded-3' style={{height:"3vh"}} /> <Skeleton
<Skeleton containerClassName='w-100' className='w-100 mb-3 rounded-4' style={{height:"25vh"}} /> containerClassName="w-100"
<Skeleton containerClassName='w-100 d-flex flex-wrap justify-content-between' className='mb-3 rounded-4' count={4} inline={true} style={{width:"49%",height:"25vh"}} /> className="w-100 mb-3 rounded-3"
</div> style={{ height: '3vh' }}
); />
} <Skeleton
containerClassName="w-100"
className="w-100 mb-3 rounded-4"
style={{ height: '25vh' }}
/>
<Skeleton
containerClassName="w-100 d-flex flex-wrap justify-content-between"
className="mb-3 rounded-4"
count={4}
inline={true}
style={{ width: '49%', height: '25vh' }}
/>
</div>
)
}
if (error) { if (error) {
return <div>Error: {error.message}</div>; return <div>Error: {error.message}</div>
} }
return ( return (
<Container className='level-page'> <Container className="level-page">
<Row> <Row>
<Col className="d-flex align-items-center mb-3 breadcrumb-con"> <Col className="d-flex align-items-center mb-3 breadcrumb-con">
<Button as={Link} variant='blue' className='btn-square-back' to={`/learning/module/${section}`}> <Button
<i className="bi bi-arrow-90deg-left"></i> as={Link}
</Button> variant="blue"
<Breadcrumb className='custom-breadcrumb'> className="btn-square-back"
<Breadcrumb.Item href="/learning/module"><i className="bi bi-book me-1"></i>Learning</Breadcrumb.Item> to={`/learning/module/${section}`}
<Breadcrumb.Item href={`/learning/module/${section}`} className='text-capitalize'>{unSlugify(section)}</Breadcrumb.Item> >
<Breadcrumb.Item active>{unSlugify(topic)}</Breadcrumb.Item> <i className="bi bi-arrow-90deg-left"></i>
</Breadcrumb> </Button>
</Col> <Breadcrumb className="custom-breadcrumb">
</Row> <Breadcrumb.Item href="/learning/module">
<i className="bi bi-book me-1"></i>Learning
{levels.length === 0 ? ( </Breadcrumb.Item>
<h1>Materi Belum Tersedia</h1> <Breadcrumb.Item
) : ( href={`/learning/module/${section}`}
<Row> className="text-capitalize"
{levels.map((level, index) => ( >
<Col key={level.ID_LEVEL} xs={12} grant={level.ID_LEVEL} sm={level.IS_PRETEST === 1 ? 12 : 6} className='mb-3'> {unSlugify(section)}
<div className={`p-3 level-con ${isLevelUnlocked(index) ? 'bg-gd' : 'bg-light-grey'} rounded-4`}> </Breadcrumb.Item>
{(!isLevelUnlocked(index) && level.SCORE > 40) && <img src={check} alt="" className='check-icon' />} <Breadcrumb.Item active>{unSlugify(topic)}</Breadcrumb.Item>
<div className="mb-3 w-100 d-flex justify-content-between"> </Breadcrumb>
{level.IS_PRETEST === 0 ?( </Col>
<div className="level-label"> </Row>
<h4 className='m-0 lh-normal'>LEVEL</h4>
<span>{index}</span>
</div>
) : (
<div className="level-label">
<h4 className='m-0 lh-normal'>PRETEST</h4>
</div>
)}
<h5 className='m-0 text-white'>Score {level.SCORE === null ? 0 : level.SCORE}/100</h5>
</div>
<p className='mb-3 text-white'>
{levelDesc[index]}
</p>
{level.IS_PRETEST === 1 && level.SCORE !== null ?( {levels.length === 0 ? (
<div className='py-2 px-5 mt-auto w-fit bg-light rounded-35'> <h1>Materi Belum Tersedia</h1>
Review ) : (
</div> <Row>
):( {levels.map((level, index) => (
!isLevelUnlocked(index) ? ( <Col
<div className='py-2 px-5 mt-auto w-fit bg-light rounded-35'> key={level.ID_LEVEL}
Not Allowed xs={12}
</div> grant={level.ID_LEVEL}
) : ( sm={level.IS_PRETEST === 1 ? 12 : 6}
// level.SCORE < 65 ?( className="mb-3"
// <Button variant='warning' className='py-2 px-5 w-fit rounded-35' >
// as={Link} to={level.IS_PRETEST === 1 ? `/learning/module/${section}/${topic}/pretest/material` : `/learning/module/${section}/${topic}/level-${index}/material`} <div
// > className={`p-3 level-con ${
// Learn Now isLevelUnlocked(index) ? 'bg-gd' : 'bg-light-grey'
// </Button> } rounded-4`}
// ) : ( >
// <div className='py-2 px-5 bg-warning w-fit rounded-35'> {!isLevelUnlocked(index) && level.SCORE > 40 && (
// Finished <img src={check} alt="" className="check-icon" />
// </div> )}
// ) <div className="mb-3 w-100 d-flex justify-content-between">
<Button variant='warning' className='py-2 px-5 w-fit rounded-35' {level.IS_PRETEST === 0 ? (
as={Link} to={level.IS_PRETEST === 1 ? `/learning/module/${section}/${topic}/pretest/material` : `/learning/module/${section}/${topic}/level-${index}/material`} <div className="level-label">
> <h4 className="m-0 lh-normal">LEVEL</h4>
Learn Now <span>{index}</span>
</Button> </div>
) ) : (
)} <div className="level-label">
<h4 className="m-0 lh-normal">PRETEST</h4>
</div>
)}
<h5 className="m-0 text-white">
Score {level.SCORE === null ? 0 : level.SCORE}/100
</h5>
</div>
<p className="mb-3 text-white">{levelDesc[index]}</p>
{level.IS_PRETEST === 1 && level.SCORE !== null ? (
<Button
variant="success"
className="py-2 px-5 w-fit rounded-35"
as={Link}
to={`/learning/review/${level.ID_STUDENT_LEARNING}`}
>
Review
</Button>
) : !isLevelUnlocked(index) ? (
<div className="py-2 px-5 mt-auto w-fit bg-light rounded-35">
Not Allowed
</div>
) : (
// level.SCORE < 65 ?(
// <Button variant='warning' className='py-2 px-5 w-fit rounded-35'
// as={Link} to={level.IS_PRETEST === 1 ? `/learning/module/${section}/${topic}/pretest/material` : `/learning/module/${section}/${topic}/level-${index}/material`}
// >
// Learn Now
// </Button>
// ) : (
// <div className='py-2 px-5 bg-warning w-fit rounded-35'>
// Finished
// </div>
// )
<Button
variant="warning"
className="py-2 px-5 w-fit rounded-35"
as={Link}
to={
level.IS_PRETEST === 1
? `/learning/module/${section}/${topic}/pretest/material`
: `/learning/module/${section}/${topic}/level-${index}/material`
}
>
Learn Now
</Button>
)}
</div>
</Col>
))}
</Row>
)}
</Container>
)
}
</div> export default Level
</Col>
))}
</Row>
)}
</Container>
);
};
export default Level;

View File

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

View File

@ -1,148 +1,193 @@
import React from 'react'; import { useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'; import { useReview } from '../hooks/useReview'
import { useReview } from '../hooks/useReview'; import {
import { Container, Row, Col, ListGroup, Button, OverlayTrigger, Popover } from 'react-bootstrap'; Container,
import MultipleChoiceQuestion from './components/MultipleChoiceQuestion'; Row,
import TrueFalseQuestion from './components/TrueFalseQuestion'; Col,
import MatchingPairsQuestion from './components/MatchingPairsQuestion'; ListGroup,
import Skeleton from 'react-loading-skeleton'; 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 Review = () => {
const { stdLearning } = useParams(); const { stdLearning } = useParams()
const { const {
questions, questions,
currentQuestion, currentQuestion,
currentQuestionIndex, currentQuestionIndex,
setCurrentQuestionIndex, setCurrentQuestionIndex,
answers, answers,
isLoading, isLoading,
error, error,
nextQuestion, nextQuestion,
prevQuestion, prevQuestion,
} = useReview(stdLearning); } = useReview(stdLearning)
const renderQuestion = () => { const renderQuestion = () => {
switch (currentQuestion.QUESTION_TYPE) { switch (currentQuestion.QUESTION_TYPE) {
case 'MCQ': case 'MCQ':
return ( return (
<MultipleChoiceQuestion <MultipleChoiceQuestion
question={currentQuestion} question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT} studentAnswer={currentQuestion.ANSWER_STUDENT}
index={currentQuestionIndex} index={currentQuestionIndex}
/> />
); )
case 'TFQ': case 'TFQ':
return ( return (
<TrueFalseQuestion <TrueFalseQuestion
question={currentQuestion} question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT} studentAnswer={currentQuestion.ANSWER_STUDENT}
index={currentQuestionIndex} index={currentQuestionIndex}
/> />
); )
case 'MPQ': case 'MPQ':
return ( return (
<MatchingPairsQuestion <MatchingPairsQuestion
question={currentQuestion} question={currentQuestion}
studentAnswer={currentQuestion.ANSWER_STUDENT} studentAnswer={currentQuestion.ANSWER_STUDENT}
index={currentQuestionIndex} index={currentQuestionIndex}
/> />
); )
default: default:
return <p>Unknown question type.</p>; return <p>Unknown question type.</p>
} }
}; }
const popover = ( const popover = (
<Popover id="popover-basic"> <Popover id="popover-basic">
<Popover.Header as="h4">Tips</Popover.Header> <Popover.Header as="h4">Tips</Popover.Header>
<Popover.Body className='p-2'> <Popover.Body className="p-2">
<ul className='ps-3 m-0'> <ul className="ps-3 m-0">
<li>Click the <strong>left arrow</strong> key to go to the previous question</li> <li>
<li>Click the <strong>right arrow</strong> key to go to the next question</li> Click the <strong>left arrow</strong> key to go to the previous
</ul> question
</Popover.Body> </li>
</Popover> <li>
); Click the <strong>right arrow</strong> key to go to the next
question
</li>
</ul>
</Popover.Body>
</Popover>
)
if (isLoading) { if (isLoading) {
return ( return (
<div className="row"> <div className="row">
<div className="col-2"> <div className="col-2">
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' count={6} style={{height:"4vh"}} /> <Skeleton
</div> containerClassName="w-100"
<div className="col-10"> className="w-100 mb-1 rounded-3"
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"5vh"}} /> count={6}
<Skeleton containerClassName='w-100' className='w-50 mb-1 rounded-3' style={{height:"20vh"}} /> style={{ height: '4vh' }}
<Skeleton containerClassName='w-100' className='w-100 mb-1 rounded-3' style={{height:"30vh"}} /> />
</div> </div>
</div> <div className="col-10">
); <Skeleton
} containerClassName="w-100"
className="w-100 mb-1 rounded-3"
if (error) return <h1 className='text-center'>Exercise questions not yet available</h1>; 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 ( if (error)
<Container fluid className='exercise-page'> return <h1 className="text-center">Exercise questions not yet available</h1>
<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;
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