Source: react/MainQuizPage.js

import React, { useState, useEffect, useCallback } from 'react';
import { AppBar, Toolbar, Typography, Button, List, ListItem, Paper, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
import { useParams, useNavigate } from 'react-router-dom';
import FlashcardRepo from '../repositories/FlashcardRepo';



//mainQuizPage component
/**
 * @class MainQuizPage
 * @classdesc MainQuizPage - A functional React component for displaying and managing a quiz.
 * Includes quiz questions display, answer selection, score calculation, and navigation controls.
 * 
 * @returns {React.Component} A component for quiz interaction and management.
 */
function MainQuizPage() {


  const { quizId } = useParams(); //aroute param to identify the quiz set
  const [questions, setQuestions] = useState([]); //array to hold question data from database
  const [selectedQuestionIndex, setSelectedQuestionIndex] = useState(null); //current selected question index
  const [score, setScore] = useState(null);//for score
  const [quizFinished, setQuizFinished] = useState(false);//check if quiz is done for return to quiz page button
  const [timeLeft, setTimeLeft] = useState(10 * 5 * 60);//default time
  const navigate = useNavigate();//navigation
  const [isPaused, setIsPaused] = useState(false);//pause quiz
  const [openDialog, setOpenDialog] = useState(false);//for dialog
  const LOCAL_STORAGE_QUIZ_KEY = 'quizPaused';//saved key
  const [openSubmitConfirm, setOpenSubmitConfirm] = useState(false);//dialog for submit quiz


  //handle time, each quiz 5 mins
  /**
  * @memberof MainQuizPage
  * @function calculateInitialTime
  * @description Calculates the initial time for the quiz based on the number of questions.
  * @returns {Number} Initial time for the quiz.
  */
  const calculateInitialTime = useCallback(() => {
    return questions.length * 5 * 60;
  }, [questions.length]);


  //open dialog
  /**
   * @memberof MainQuizPage
   * @function handleOpenDialog
   * @description Opens the dialog for pausing the quiz.
   */
  const handleOpenDialog = () => {
    setOpenDialog(true);
  };


  //close dialog
  /**
  * @memberof MainQuizPage
  * @function handleCloseDialog
  * @description Closes the dialog for pausing the quiz.
  */
  const handleCloseDialog = () => {
    setOpenDialog(false);
  };


  //for pausing
  /**
  * @memberof MainQuizPage
  * @function handlePause
  * @description Handles the action to pause the quiz.
  */
  const handlePause = () => {
    handleOpenDialog();
  };


  //confirm pause
  /**
  * @memberof MainQuizPage
  * @function handleConfirmPause
  * @description Confirms the pause action and saves the current state of the quiz.
  */
  const handleConfirmPause = () => {
    const quizState = {
      selectedQuestionIndex,
      questions,
      timeLeft
    };
    localStorage.setItem(LOCAL_STORAGE_QUIZ_KEY, JSON.stringify({ quizId, paused: true, quizState }));
    setIsPaused(true);
    handleCloseDialog();
  };


  //for resume, with saved status
  /**
 * @memberof MainQuizPage
 * @function handleResume
 * @description Resumes the quiz from the saved state.
 */
  const handleResume = () => {
    setIsPaused(false);
    const savedState = JSON.parse(localStorage.getItem(LOCAL_STORAGE_QUIZ_KEY));
    if (savedState && savedState.quizId === quizId) {
      setSelectedQuestionIndex(savedState.quizState.selectedQuestionIndex);
      setQuestions(savedState.quizState.questions);
      setTimeLeft(savedState.quizState.timeLeft);
    } else {
      console.log('No saved quiz state found');
    }
  };


  //for randomly diplay answer option so each time they not in the same spot
  /**
 * @memberof MainQuizPage
 * @function shuffleChoices
 * @description Randomly shuffles the choice options for each question.
 * @param {Array} choices - Array of choice options to shuffle.
 * @returns {Array} Shuffled choice options.
 */
  const shuffleChoices = useCallback((choices) => {
    for (let i = choices.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [choices[i], choices[j]] = [choices[j], choices[i]];
    }
    return choices;
  }, []);


  //save
  useEffect(() => {
    const savedState = JSON.parse(localStorage.getItem(LOCAL_STORAGE_QUIZ_KEY));
    if (savedState && savedState.quizId === quizId) {
      setIsPaused(savedState.paused);
      setSelectedQuestionIndex(savedState.quizState.selectedQuestionIndex);
      setQuestions(savedState.quizState.questions);
      setTimeLeft(savedState.quizState.timeLeft);
    }
  }, [quizId]);


  //fetch question from database, include questions and answers
  useEffect(() => {
    const fetchQuestions = async () => {
      try {
        const questionData = await FlashcardRepo.getQuestionItems(quizId);
        const questionsArray = Object.keys(questionData).map(key => {
          const correctAnswerIndex = questionData[key].correctChoice;
          const correctAnswer = questionData[key].choices[correctAnswerIndex];
          const shuffledChoices = shuffleChoices([...questionData[key].choices]);
          const newCorrectIndex = shuffledChoices.indexOf(correctAnswer);
          return {
            ...questionData[key],
            choices: shuffledChoices,
            correctChoice: newCorrectIndex,
            userAnswer: null
          };
        });
        setQuestions(questionsArray);
        setSelectedQuestionIndex(0);
        setTimeLeft(questionsArray.length * 5 * 60);
      } catch (error) {
        console.error("Failed to fetch questions:", error);
      }
    };

    fetchQuestions();
  },
    [quizId, shuffleChoices]);


  //update timeleft when change length of question
  useEffect(() => {
    setTimeLeft(calculateInitialTime());
  }, [calculateInitialTime, questions.length]);

  useEffect(() => {
    let timer = null;
    if (timeLeft > 0 && !isPaused && !quizFinished) {
      timer = setInterval(() => {
        setTimeLeft(prevTimeLeft => prevTimeLeft - 1);
      }, 1000);
    } else {
      clearInterval(timer);
    }
    return () => clearInterval(timer);
  }, [timeLeft, isPaused, quizFinished]);


  //time format
  /**
 * @memberof MainQuizPage
 * @function formatTime
 * @description Formats the remaining time for display.
 * @returns {String} Formatted time string.
 */
  const formatTime = () => {
    const minutes = Math.floor(timeLeft / 60);
    const seconds = timeLeft % 60;
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  };


  //check if answer is correct
  /**
 * @memberof MainQuizPage
 * @function checkAnswer
 * @description Checks if the selected answer is correct and updates the question state.
 * @param {Number} choiceIndex - Index of the selected choice.
 */
  const checkAnswer = (choiceIndex) => {
    setQuestions(prevQuestions => {
      return prevQuestions.map((question, index) => {
        if (index === selectedQuestionIndex) {
          const isCorrect = choiceIndex === question.correctChoice;
          return {
            ...question,
            userAnswer:
              choiceIndex,
            isCorrect: isCorrect,
          };
        }
        return question;
      });
    });
  };


  //calculate score

  const calculateScore = () => {
    const correctAnswers = questions.reduce((acc, question) => {
      return acc + (question.userAnswer === question.correctChoice ? 1 : 0);
    }, 0);
    const scorePercentage = (correctAnswers / questions.length) * 100;
    return scorePercentage;
  };

  //open submit dialog
  const handleSubmit = () => {
    setOpenSubmitConfirm(true);
  };


  // for submit
  // Modify handleConfirmSubmit to use the returned score from calculateScore
  const handleConfirmSubmit = async () => {
    const calculatedScore = calculateScore(); // Get the score directly
    setQuizFinished(true);

    if (calculatedScore !== null) {
      try {
        const newAttemptId = await FlashcardRepo.updateScoreAndAddAttempt(quizId, calculatedScore);
        console.log(`New attempt recorded with ID: ${newAttemptId}`);
        setScore(calculatedScore); // Now you can update the state with the calculated score
      } catch (error) {
        console.error("Failed to update score and attempt:", error);
      }
    } else {
      console.error("Score calculation failed, cannot update score and attempt.");
    }

    localStorage.removeItem(LOCAL_STORAGE_QUIZ_KEY);
    setOpenSubmitConfirm(false);
  };





  //for previous button
  /**
 * @memberof MainQuizPage
 * @function handlePrevious
 * @description Navigates to the previous question.
 */
  const handlePrevious = () => {
    setSelectedQuestionIndex(prevIndex => Math.max(prevIndex - 1, 0));
  };


  //for next button
  /**
 * @memberof MainQuizPage
 * @function handleNext
 * @description Navigates to the next question.
 */
  const handleNext = () => {
    setSelectedQuestionIndex(prevIndex => Math.min(prevIndex + 1, questions.length - 1));
  };


  //back button 
  /**
 * @memberof MainQuizPage
 * @function handleBack
 * @description Navigates back to the flashcards page.
 */
  const handleBack = () => {
    navigate('/flashcard'); //navigate to the flashcards page
  };


  //mark options
  /**
 * @memberof MainQuizPage
 * @function getButtonStyle
 * @description Determines the style for each answer choice button.
 * @param {Number} choiceIndex - Index of the current choice.
 * @param {Number} questionIndex - Index of the current question.
 * @returns {Object} Style object for the button.
 */
  const getButtonStyle = (choiceIndex, questionIndex) => {
    if (questions.length > 0) {
      const question = questions[questionIndex];
      const isSelected = question.userAnswer !== null && choiceIndex === question.userAnswer;
      const isCorrect = quizFinished && choiceIndex === question.correctChoice;
      if (isCorrect) {
        return { backgroundColor: 'green', color: 'white' };
      } else if (isSelected) {
        return { backgroundColor: '#1976d2', color: 'red' };
      }
    }
    return {};
  };


  //function to determine the style for each question in the sidebar,answered and not answered
  /**
 * @memberof MainQuizPage
 * @function getQuestionStyle
 * @description Determines the style for each question in the sidebar.
 * @param {Number} index - Index of the question.
 * @returns {Object} Style object for the question.
 */
  const getQuestionStyle = (index) => {
    if (questions[index].userAnswer !== null) {
      return { color: 'green' };
    } else {
      return { color: 'black' };
    }
  };


  return (
    <div>
      {/*appBar for the main header */}
      <AppBar position="static">
        <Toolbar style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <Typography variant="h4" component="h2" style={{ margin: 0 }}>
            Quiz
          </Typography>
          <div style={{ position: 'relative' }}>
            {!quizFinished && (
              <>
                {/*timer display */}
                <Typography variant="h6" style={{ marginRight: '20px' }}>
                  Time Left: {formatTime()}
                </Typography>
              </>
            )}
          </div>
          {/*submit button */}
          <Button
            variant="contained"
            color="primary"
            onClick={handleSubmit}
            style={{ position: 'absolute', top: '100px', right: '0' }}
            disabled={isPaused}>
            Submit Quiz
          </Button>

          {/*pause and resume button*/}
          {!quizFinished && !isPaused && (
            <Button
              variant="contained"
              color="primary"
              onClick={handlePause}>
              Pause Quiz
            </Button>
          )}
          {!quizFinished && isPaused && (
            <Button
              variant="contained"
              color="primary"
              onClick={handleResume}>
              resume
            </Button>
          )}
        </Toolbar>
      </AppBar>

      <div style={{ display: 'flex', marginTop: '20px' }}>
        {/*sidebar for questions */}
        <Paper elevation={3} style={{ width: '20%', maxHeight: '100vh', overflow: 'auto', padding: '10px' }}>
          <List>
            {questions.map((_, index) => (
              <ListItem
                button
                key={index}
                onClick={() => setSelectedQuestionIndex(index)}
                style={getQuestionStyle(index)}
              >
                {`Question ${index + 1}`}
              </ListItem>
            ))}
          </List>
        </Paper>

        {/*main content area for displaying questions and answer options */}
        <div style={{ flex: 1, padding: '20px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
          {selectedQuestionIndex !== null && questions[selectedQuestionIndex] && (
            <>
              {/*question display */}
              <Typography variant="h4" component="h2" style={{ marginBottom: '30px' }}>
                {`Question ${selectedQuestionIndex + 1}`}
              </Typography>
              <Typography variant="h6" component="h2" style={{ marginBottom: '30px' }}>
                {questions[selectedQuestionIndex].question}
              </Typography>

              {/*answer options */}
              <div style={{
                display: 'grid',
                gridTemplateColumns: 'repeat(2, 1fr)',
                gap: '10px',
                marginTop: '20px'
              }}>
                {questions[selectedQuestionIndex].choices.map((choice, index) => (
                  <Button
                    key={index}
                    variant="contained"
                    style={getButtonStyle(index, selectedQuestionIndex)}
                    onClick={() => checkAnswer(index)}
                    disabled={quizFinished || isPaused}
                  >
                    {choice}
                  </Button>
                ))}
              </div>
            </>
          )}
        </div>
      </div>

      {/*previous and next buttons */}
      <div style={{ marginTop: '30px' }}>
        <Button variant="contained" color="primary" onClick={handlePrevious} disabled={selectedQuestionIndex === 0}>
          Previous
        </Button>

        <Button variant="contained" color="primary" onClick={handleNext} disabled={selectedQuestionIndex === questions.length - 1} style={{ marginLeft: '10px' }}>
          Next
        </Button>
      </div>

      {/*display score and button group*/}
      {quizFinished && (
        <div style={{ textAlign: 'center', marginTop: '20px' }}>
          <Typography variant="h4" component="h2">
            {`Your score: ${score ? score.toFixed(2) : 0}%`}
          </Typography>

          {/*button group*/}
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '10px' }}>


            {/*back button */}
            <Button
              variant="contained"
              color="secondary"
              onClick={handleBack}
            >
              Back
            </Button>
          </div>
        </div>
      )}

      {/*submit Quiz Confirmation Dialog */}
      <Dialog open={openSubmitConfirm} onClose={() => setOpenSubmitConfirm(false)}>
        <DialogTitle>Submit Quiz</DialogTitle>
        <DialogContent>
          <DialogContentText>
            Are you sure you want to submit the quiz?
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setOpenSubmitConfirm(false)} color="primary">
            No
          </Button>
          <Button onClick={handleConfirmSubmit} color="primary">
            Yes
          </Button>
        </DialogActions>
      </Dialog>

      {/*pause quiz dialog*/}
      <Dialog
        open={openDialog}
        onClose={handleCloseDialog}
        aria-labelledby="alert-dialog-title"
        aria-describedby="alert-dialog-description">

        <DialogTitle id="alert-dialog-title">{"Pause Quiz"}</DialogTitle>
        <DialogContent>
          <DialogContentText id="alert-dialog-description">
            Are you sure you want to pause the quiz?
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleConfirmPause} color="primary" autoFocus>
            Yes
          </Button>
          <Button onClick={handleCloseDialog} color="primary">
            No
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}

export default MainQuizPage;